22
22
from bzrlib.dirstate import DirState
25
# Give Pyrex some function definitions for it to understand.
26
# All of these are just hints to Pyrex, so that it can try to convert python
27
# objects into similar C objects. (such as PyInt => int).
28
# In anything defined 'cdef extern from XXX' the real C header will be
29
# imported, and the real definition will be used from there. So these are just
30
# hints, and do not need to match exactly to the C definitions.
25
32
cdef extern from *:
33
ctypedef unsigned long size_t
35
cdef extern from "_dirstate_helpers_c.h":
39
cdef extern from "stdlib.h":
40
unsigned long int strtoul(char *nptr, char **endptr, int base)
43
# These functions allow us access to a bit of the 'bare metal' of python
44
# objects, rather than going through the object abstraction. (For example,
45
# PyList_Append, rather than getting the 'append' attribute of the object, and
46
# creating a tuple, and then using PyCallObject).
47
# Functions that return (or take) a void* are meant to grab a C PyObject*. This
48
# differs from the Pyrex 'object'. If you declare a variable as 'object' Pyrex
49
# will automatically Py_INCREF and Py_DECREF when appropriate. But for some
50
# inner loops, we don't need to do that at all, as the reference only lasts for
28
52
cdef extern from "Python.h":
29
# GetItem returns a borrowed reference
30
void *PyDict_GetItem(object p, object key)
31
int PyDict_SetItem(object p, object key, object val) except -1
33
53
int PyList_Append(object lst, object item) except -1
34
54
void *PyList_GetItem_object_void "PyList_GET_ITEM" (object lst, int index)
35
object PyList_GET_ITEM (object lst, int index)
36
55
int PyList_CheckExact(object)
38
int PyTuple_CheckExact(object)
39
57
void *PyTuple_GetItem_void_void "PyTuple_GET_ITEM" (void* tpl, int index)
40
object PyTuple_New(int)
41
int PyTuple_SetItem(object tpl, int offset, object val)
42
void PyTuple_SET_ITEM(object tpl, int offset, object val)
43
object PyTuple_Pack(int n, ...)
45
59
char *PyString_AsString(object p)
46
60
char *PyString_AS_STRING_void "PyString_AS_STRING" (void *p)
61
object PyString_FromString(char *)
62
object PyString_FromStringAndSize(char *, int)
47
63
int PyString_Size(object p)
48
64
int PyString_GET_SIZE_void "PyString_GET_SIZE" (void *p)
49
65
int PyString_CheckExact(object p)
51
void Py_INCREF(object)
52
void Py_DECREF(object)
55
cdef extern from "stdlib.h":
56
unsigned long int strtoul(char *nptr, char **endptr, int base)
58
68
cdef extern from "string.h":
59
char *strchr(char *s1, char c)
62
cdef object _split_from_path(object cache, object path):
63
"""get the dirblock tuple for a given path.
65
:param cache: A Dictionary mapping string paths to tuples
66
:param path: The path we care about.
67
:return: A borrowed reference to a tuple stored in cache.
68
You do not need to Py_DECREF() when you are done, unless you plan on
74
value_ptr = PyDict_GetItem(cache, path)
76
value = path.split('/')
79
value = <object>value_ptr
84
cdef int _cmp_dirblock_strings(char *path1, int size1, char *path2, int size2):
69
int strncmp(char *s1, char *s2, int len)
70
void *memchr(void *s, int c, size_t len)
71
int memcmp(void *b1, void *b2, size_t len)
72
# ??? memrchr is a GNU extension :(
73
# void *memrchr(void *s, int c, size_t len)
76
cdef void* _my_memrchr(void *s, int c, size_t n):
77
# memrchr seems to be a GNU extension, so we have to implement it ourselves
90
def _py_memrchr(s, c):
91
"""Just to expose _my_memrchr for testing.
93
:param s: The Python string to search
94
:param c: The character to search for
95
:return: The offset to the last instance of 'c' in s
102
_s = PyString_AsString(s)
103
length = PyString_Size(s)
105
_c = PyString_AsString(c)
106
assert PyString_Size(c) == 1,\
107
'Must be a single character string, not %s' % (c,)
108
found = _my_memrchr(_s, _c[0], length)
111
return <char*>found - <char*>_s
114
cdef int _is_aligned(void *ptr):
115
"""Is this pointer aligned to an integer size offset?
117
:return: 1 if this pointer is aligned, 0 otherwise.
119
return ((<intptr_t>ptr) & ((sizeof(int))-1)) == 0
122
cdef int _cmp_by_dirs(char *path1, int size1, char *path2, int size2):
123
cdef unsigned char *cur1
124
cdef unsigned char *cur2
125
cdef unsigned char *end1
126
cdef unsigned char *end2
89
127
cdef int *cur_int1
90
128
cdef int *cur_int2
91
129
cdef int *end_int1
92
130
cdef int *end_int2
94
cur_int1 = <int*>path1
95
cur_int2 = <int*>path2
96
end_int1 = <int*>(path1 + size1 - (size1%4))
97
end_int2 = <int*>(path2 + size2 - (size2%4))
132
if path1 == path2 and size1 == size2:
135
end1 = <unsigned char*>path1+size1
136
end2 = <unsigned char*>path2+size2
101
138
# Use 32-bit comparisons for the matching portion of the string.
102
139
# Almost all CPU's are faster at loading and comparing 32-bit integers,
103
140
# than they are at 8-bit integers.
104
while cur_int1 < end_int1 and cur_int2 < end_int2:
105
if cur_int1[0] != cur_int2[0]:
107
cur_int1 = cur_int1 + 1
108
cur_int2 = cur_int2 + 1
110
cur1 = <char*>cur_int1
111
cur2 = <char*>cur_int2
141
# 99% of the time, these will be aligned, but in case they aren't just skip
143
if _is_aligned(path1) and _is_aligned(path2):
144
cur_int1 = <int*>path1
145
cur_int2 = <int*>path2
146
end_int1 = <int*>(path1 + size1 - (size1 % sizeof(int)))
147
end_int2 = <int*>(path2 + size2 - (size2 % sizeof(int)))
149
while cur_int1 < end_int1 and cur_int2 < end_int2:
150
if cur_int1[0] != cur_int2[0]:
152
cur_int1 = cur_int1 + 1
153
cur_int2 = cur_int2 + 1
155
cur1 = <unsigned char*>cur_int1
156
cur2 = <unsigned char*>cur_int2
158
cur1 = <unsigned char*>path1
159
cur2 = <unsigned char*>path2
113
161
while cur1 < end1 and cur2 < end2:
114
162
if cur1[0] == cur2[0]:
138
def cmp_dirblock_strings(path1, path2):
139
"""Compare to python strings in dirblock fashion."""
140
return _cmp_dirblock_strings(PyString_AsString(path1),
186
def cmp_by_dirs_c(path1, path2):
187
"""Compare two paths directory by directory.
189
This is equivalent to doing::
191
cmp(path1.split('/'), path2.split('/'))
193
The idea is that you should compare path components separately. This
194
differs from plain ``cmp(path1, path2)`` for paths like ``'a-b'`` and
195
``a/b``. "a-b" comes after "a" but would come before "a/b" lexically.
197
:param path1: first path
198
:param path2: second path
199
:return: negative number if ``path1`` comes first,
200
0 if paths are equal,
201
and positive number if ``path2`` sorts first
203
if not PyString_CheckExact(path1):
204
raise TypeError("'path1' must be a plain string, not %s: %r"
205
% (type(path1), path1))
206
if not PyString_CheckExact(path2):
207
raise TypeError("'path2' must be a plain string, not %s: %r"
208
% (type(path2), path2))
209
return _cmp_by_dirs(PyString_AsString(path1),
210
PyString_Size(path1),
211
PyString_AsString(path2),
212
PyString_Size(path2))
215
def _cmp_path_by_dirblock_c(path1, path2):
216
"""Compare two paths based on what directory they are in.
218
This generates a sort order, such that all children of a directory are
219
sorted together, and grandchildren are in the same order as the
220
children appear. But all grandchildren come after all children.
222
In other words, all entries in a directory are sorted together, and
223
directorys are sorted in cmp_by_dirs order.
225
:param path1: first path
226
:param path2: the second path
227
:return: negative number if ``path1`` comes first,
229
and a positive number if ``path2`` sorts first
231
if not PyString_CheckExact(path1):
232
raise TypeError("'path1' must be a plain string, not %s: %r"
233
% (type(path1), path1))
234
if not PyString_CheckExact(path2):
235
raise TypeError("'path2' must be a plain string, not %s: %r"
236
% (type(path2), path2))
237
return _cmp_path_by_dirblock(PyString_AsString(path1),
141
238
PyString_Size(path1),
142
239
PyString_AsString(path2),
143
240
PyString_Size(path2))
146
def c_bisect_dirblock(dirblocks, dirname, lo=0, hi=None, cache=None):
243
cdef int _cmp_path_by_dirblock(char *path1, int path1_len,
244
char *path2, int path2_len):
245
"""Compare two paths by what directory they are in.
247
see ``_cmp_path_by_dirblock_c`` for details.
250
cdef int dirname1_len
252
cdef int dirname2_len
254
cdef int basename1_len
256
cdef int basename2_len
260
if path1_len == 0 and path2_len == 0:
263
if path1 == path2 and path1_len == path2_len:
272
basename1 = <char*>_my_memrchr(path1, c'/', path1_len)
274
if basename1 == NULL:
276
basename1_len = path1_len
281
dirname1_len = basename1 - path1
282
basename1 = basename1 + 1
283
basename1_len = path1_len - dirname1_len - 1
285
basename2 = <char*>_my_memrchr(path2, c'/', path2_len)
287
if basename2 == NULL:
289
basename2_len = path2_len
294
dirname2_len = basename2 - path2
295
basename2 = basename2 + 1
296
basename2_len = path2_len - dirname2_len - 1
298
cmp_val = _cmp_by_dirs(dirname1, dirname1_len,
299
dirname2, dirname2_len)
303
cur_len = basename1_len
304
if basename2_len < basename1_len:
305
cur_len = basename2_len
307
cmp_val = memcmp(basename1, basename2, cur_len)
310
if basename1_len == basename2_len:
312
if basename1_len < basename2_len:
317
def _bisect_path_left_c(paths, path):
318
"""Return the index where to insert path into paths.
320
This uses a path-wise comparison so we get::
330
:param paths: A list of paths to search through
331
:param path: A single path to insert
332
:return: An offset where 'path' can be inserted.
333
:seealso: bisect.bisect_left
344
if not PyList_CheckExact(paths):
345
raise TypeError("you must pass a python list for 'paths' not: %s %r"
346
% (type(paths), paths))
347
if not PyString_CheckExact(path):
348
raise TypeError("you must pass a string for 'path' not: %s %r"
349
% (type(path), path))
354
path_cstr = PyString_AsString(path)
355
path_size = PyString_Size(path)
358
_mid = (_lo + _hi) / 2
359
cur = PyList_GetItem_object_void(paths, _mid)
360
cur_cstr = PyString_AS_STRING_void(cur)
361
cur_size = PyString_GET_SIZE_void(cur)
362
if _cmp_path_by_dirblock(cur_cstr, cur_size, path_cstr, path_size) < 0:
369
def _bisect_path_right_c(paths, path):
370
"""Return the index where to insert path into paths.
372
This uses a path-wise comparison so we get::
382
:param paths: A list of paths to search through
383
:param path: A single path to insert
384
:return: An offset where 'path' can be inserted.
385
:seealso: bisect.bisect_right
396
if not PyList_CheckExact(paths):
397
raise TypeError("you must pass a python list for 'paths' not: %s %r"
398
% (type(paths), paths))
399
if not PyString_CheckExact(path):
400
raise TypeError("you must pass a string for 'path' not: %s %r"
401
% (type(path), path))
406
path_cstr = PyString_AsString(path)
407
path_size = PyString_Size(path)
410
_mid = (_lo + _hi) / 2
411
cur = PyList_GetItem_object_void(paths, _mid)
412
cur_cstr = PyString_AS_STRING_void(cur)
413
cur_size = PyString_GET_SIZE_void(cur)
414
if _cmp_path_by_dirblock(path_cstr, path_size, cur_cstr, cur_size) < 0:
421
def bisect_dirblock_c(dirblocks, dirname, lo=0, hi=None, cache=None):
147
422
"""Return the index where to insert dirname into the dirblocks.
149
424
The return value idx is such that all directories blocks in dirblock[:idx]
159
cdef char *dirname_str
434
cdef char *dirname_cstr
160
435
cdef int dirname_size
162
437
cdef int cur_size
440
if not PyList_CheckExact(dirblocks):
441
raise TypeError("you must pass a python list for 'dirblocks' not: %s %r"
442
% (type(dirblocks), dirblocks))
443
if not PyString_CheckExact(dirname):
444
raise TypeError("you must pass a string for dirname not: %s %r"
445
% (type(dirname), dirname))
166
447
_hi = len(dirblocks)
170
if not PyList_CheckExact(dirblocks):
171
raise TypeError('you must pass a python list for dirblocks')
173
if not PyString_CheckExact(dirname):
174
raise TypeError('you must pass a string for dirname')
175
dirname_str = PyString_AsString(dirname)
452
dirname_cstr = PyString_AsString(dirname)
176
453
dirname_size = PyString_Size(dirname)
456
_mid = (_lo + _hi) / 2
180
457
# Grab the dirname for the current dirblock
181
458
# cur = dirblocks[_mid][0]
182
459
cur = PyTuple_GetItem_void_void(
183
460
PyList_GetItem_object_void(dirblocks, _mid), 0)
184
cur_str = PyString_AS_STRING_void(cur)
461
cur_cstr = PyString_AS_STRING_void(cur)
185
462
cur_size = PyString_GET_SIZE_void(cur)
186
if _cmp_dirblock_strings(cur_str, cur_size,
187
dirname_str, dirname_size) < 0:
463
if _cmp_by_dirs(cur_cstr, cur_size, dirname_cstr, dirname_size) < 0:
194
cdef object _List_GetItem_Incref(object lst, int offset):
195
"""Get an item, and increment a reference to it.
197
The caller must have checked that the object really is a list.
200
cur = PyList_GET_ITEM(lst, offset)
205
cdef object _fields_to_entry_0_parents(object fields):
206
cdef object path_name_file_id_key
208
cdef unsigned long int size
209
cdef char* executable_str
210
cdef object is_executable
211
if not PyList_CheckExact(fields):
212
raise TypeError('fields must be a list')
213
path_name_file_id_key = (_List_GetItem_Incref(fields, 0),
214
_List_GetItem_Incref(fields, 1),
215
_List_GetItem_Incref(fields, 2),
218
size_str = PyString_AS_STRING_void(
219
PyList_GetItem_object_void(fields, 5))
220
size = strtoul(size_str, NULL, 10)
221
executable_str = PyString_AS_STRING_void(
222
PyList_GetItem_object_void(fields, 6))
223
if executable_str[0] == c'y':
226
is_executable = False
227
return (path_name_file_id_key, [
229
_List_GetItem_Incref(fields, 3),# minikind
230
_List_GetItem_Incref(fields, 4),# fingerprint
232
is_executable, # executable
233
_List_GetItem_Incref(fields, 7),# packed_stat or revision_id
237
def _c_read_dirblocks(state):
471
"""Maintain the current location, and return fields as you parse them."""
473
cdef object text # The overall string object
474
cdef char *text_cstr # Pointer to the beginning of text
475
cdef int text_size # Length of text
477
cdef char *end_cstr # End of text
478
cdef char *cur_cstr # Pointer to the current record
479
cdef char *next # Pointer to the end of this record
481
def __init__(self, text):
483
self.text_cstr = PyString_AsString(text)
484
self.text_size = PyString_Size(text)
485
self.end_cstr = self.text_cstr + self.text_size
486
self.cur_cstr = self.text_cstr
488
cdef char *get_next(self, int *size):
489
"""Return a pointer to the start of the next field."""
492
self.cur_cstr = <char*>memchr(next, c'\0', self.end_cstr-next)
493
size[0] = self.cur_cstr - next
494
self.cur_cstr = self.cur_cstr + 1
497
cdef object get_next_str(self):
498
"""Get the next field as a Python string."""
501
next = self.get_next(&size)
502
return PyString_FromStringAndSize(next, size)
504
cdef int _init(self) except -1:
505
"""Get the pointer ready.
507
This assumes that the dirstate header has already been read, and we
508
already have the dirblock string loaded into memory.
509
This just initializes our memory pointers, etc for parsing of the
514
# The first field should be an empty string left over from the Header
515
first = self.get_next(&size)
516
if first[0] != c'\0' and size == 0:
517
raise AssertionError('First character should be null not: %s'
521
cdef object _get_entry(self, int num_trees, void **p_current_dirname,
523
"""Extract the next entry.
525
This parses the next entry based on the current location in
527
Each entry can be considered a "row" in the total table. And each row
528
has a fixed number of columns. It is generally broken up into "key"
529
columns, then "current" columns, and then "parent" columns.
531
:param num_trees: How many parent trees need to be parsed
532
:param p_current_dirname: A pointer to the current PyString
533
representing the directory name.
534
We pass this in as a void * so that pyrex doesn't have to
535
increment/decrement the PyObject reference counter for each
537
We use a pointer so that _get_entry can update it with the new
539
:param new_block: This is to let the caller know that it needs to
540
create a new directory block to store the next entry.
542
cdef object path_name_file_id_key
543
cdef char *entry_size_cstr
544
cdef unsigned long int entry_size
545
cdef char* executable_cstr
546
cdef int is_executable
547
cdef char* dirname_cstr
552
cdef object fingerprint
555
# Read the 'key' information (dirname, name, file_id)
556
dirname_cstr = self.get_next(&cur_size)
557
# Check to see if we have started a new directory block.
558
# If so, then we need to create a new dirname PyString, so that it can
559
# be used in all of the tuples. This saves time and memory, by re-using
560
# the same object repeatedly.
562
# Do the cheap 'length of string' check first. If the string is a
563
# different length, then we *have* to be a different directory.
564
if (cur_size != PyString_GET_SIZE_void(p_current_dirname[0])
565
or strncmp(dirname_cstr,
566
# Extract the char* from our current dirname string. We
567
# know it is a PyString, so we can use
568
# PyString_AS_STRING, we use the _void version because
569
# we are tricking Pyrex by using a void* rather than an
571
PyString_AS_STRING_void(p_current_dirname[0]),
573
dirname = PyString_FromStringAndSize(dirname_cstr, cur_size)
574
p_current_dirname[0] = <void*>dirname
579
# Build up the key that will be used.
580
# By using <object>(void *) Pyrex will automatically handle the
581
# Py_INCREF that we need.
582
path_name_file_id_key = (<object>p_current_dirname[0],
587
# Parse all of the per-tree information. current has the information in
588
# the same location as parent trees. The only difference is that 'info'
589
# is a 'packed_stat' for current, while it is a 'revision_id' for
591
# minikind, fingerprint, and info will be returned as regular python
593
# entry_size and is_executable will be parsed into a python Long and
594
# python Boolean, respectively.
595
# TODO: jam 20070718 Consider changin the entry_size conversion to
596
# prefer python Int when possible. They are generally faster to
597
# work with, and it will be rare that we have a file >2GB.
598
# Especially since this code is pretty much fixed at a max of
601
for i from 0 <= i < num_trees:
602
minikind = self.get_next_str()
603
fingerprint = self.get_next_str()
604
entry_size_cstr = self.get_next(&cur_size)
605
entry_size = strtoul(entry_size_cstr, NULL, 10)
606
executable_cstr = self.get_next(&cur_size)
607
is_executable = (executable_cstr[0] == c'y')
608
info = self.get_next_str()
609
PyList_Append(trees, (
611
fingerprint, # fingerprint
613
is_executable,# executable
614
info, # packed_stat or revision_id
617
# The returned tuple is (key, [trees])
618
ret = (path_name_file_id_key, trees)
619
# Ignore the trailing newline, but assert that it does exist, this
620
# ensures that we always finish parsing a line on an end-of-entry
622
trailing = self.get_next(&cur_size)
623
if cur_size != 1 or trailing[0] != c'\n':
624
raise AssertionError(
625
'Bad parse, we expected to end on \\n, not: %d %s: %s'
626
% (cur_size, PyString_FromStringAndSize(trailing, cur_size),
630
def _parse_dirblocks(self, state):
631
"""Parse all dirblocks in the state file."""
633
cdef object current_block
635
cdef void * current_dirname
637
cdef int expected_entry_count
640
num_trees = state._num_present_parents() + 1
641
expected_entry_count = state._num_entries
643
# Ignore the first record
647
state._dirblocks = [('', current_block), ('', [])]
649
current_dirname = <void*>obj
653
# TODO: jam 2007-05-07 Consider pre-allocating some space for the
654
# members, and then growing and shrinking from there. If most
655
# directories have close to 10 entries in them, it would save a
656
# few mallocs if we default our list size to something
657
# reasonable. Or we could malloc it to something large (100 or
658
# so), and then truncate. That would give us a malloc + realloc,
659
# rather than lots of reallocs.
660
while self.cur_cstr < self.end_cstr:
661
entry = self._get_entry(num_trees, ¤t_dirname, &new_block)
663
# new block - different dirname
665
PyList_Append(state._dirblocks,
666
(<object>current_dirname, current_block))
667
PyList_Append(current_block, entry)
668
entry_count = entry_count + 1
669
if entry_count != expected_entry_count:
670
raise AssertionError('We read the wrong number of entries.'
671
' We expected to read %s, but read %s'
672
% (expected_entry_count, entry_count))
673
state._split_root_dirblock_into_contents()
676
def _read_dirblocks_c(state):
238
677
"""Read in the dirblocks for the given DirState object.
240
679
This is tightly bound to the DirState internal representation. It should be
244
683
:param state: A DirState object.
685
:postcondition: The dirblocks will be loaded into the appropriate fields in
251
cdef int num_present_parents
253
688
state._state_file.seek(state._end_of_header)
254
689
text = state._state_file.read()
255
690
# TODO: check the crc checksums. crc_measured = zlib.crc32(text)
257
fields = text.split('\0')
258
# Remove the last blank entry
259
trailing = fields.pop()
260
assert trailing == ''
261
# consider turning fields into a tuple.
263
# skip the first field which is the trailing null from the header.
265
# Each line now has an extra '\n' field which is not used
266
# so we just skip over it
268
# 3 fields for the key
269
# + number of fields per tree_data (5) * tree count
271
num_present_parents = state._num_present_parents()
272
tree_count = 1 + num_present_parents
273
entry_size = state._fields_per_entry()
274
expected_field_count = entry_size * state._num_entries
275
field_count = len(fields)
276
# this checks our adjustment, and also catches file too short.
277
assert field_count - cur == expected_field_count, \
278
'field count incorrect %s != %s, entry_size=%s, '\
279
'num_entries=%s fields=%r' % (
280
field_count - cur, expected_field_count, entry_size,
281
state._num_entries, fields)
283
if num_present_parents == 1:
284
# Bind external functions to local names
286
# We access all fields in order, so we can just iterate over
287
# them. Grab an straight iterator over the fields. (We use an
288
# iterator because we don't want to do a lot of additions, nor
289
# do we want to do a lot of slicing)
290
next = iter(fields).next
291
# Move the iterator to the current position
292
for x in xrange(cur):
294
# The two blocks here are deliberate: the root block and the
295
# contents-of-root block.
296
state._dirblocks = [('', []), ('', [])]
297
current_block = state._dirblocks[0][1]
299
append_entry = current_block.append
300
for count in xrange(state._num_entries):
304
if dirname != current_dirname:
305
# new block - different dirname
307
current_dirname = dirname
308
state._dirblocks.append((current_dirname, current_block))
309
append_entry = current_block.append
310
# we know current_dirname == dirname, so re-use it to avoid
311
# creating new strings
312
entry = ((current_dirname, name, file_id),
315
next(), # fingerprint
317
next() == 'y', # executable
318
next(), # packed_stat or revision_id
322
next(), # fingerprint
324
next() == 'y', # executable
325
next(), # packed_stat or revision_id
329
assert trailing == '\n'
330
# append the entry to the current block
332
state._split_root_dirblock_into_contents()
333
elif num_present_parents == 0:
336
while pos < field_count:
337
PyList_Append(entries,
338
_fields_to_entry_0_parents(fields[pos:pos+entry_size]))
339
pos = pos + entry_size
340
state._entries_to_current_state(entries)
342
fields_to_entry = state._get_fields_to_entry()
344
entries_append = entries.append
346
while pos < field_count:
347
entries_append(fields_to_entry(fields[pos:pos+entry_size]))
348
pos = pos + entry_size
349
state._entries_to_current_state(entries)
350
# To convert from format 2 => format 3
351
# state._dirblocks = sorted(state._dirblocks,
352
# key=lambda blk:blk[0].split('/'))
353
# To convert from format 3 => format 2
354
# state._dirblocks = sorted(state._dirblocks)
692
reader = Reader(text)
694
reader._parse_dirblocks(state)
355
695
state._dirblock_state = DirState.IN_MEMORY_UNMODIFIED