2 * Copyright (c) 2017 Apple Inc. All rights reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
30 #include <sys/fsctl.h>
31 #include <sys/vnode.h>
32 #include <sys/errno.h>
34 #include <os/assumes.h>
36 #include <TargetConditionals.h>
39 #include "dirstat_collection.h"
41 #if !TARGET_OS_SIMULATOR
46 #include <apfs/apfs_fsctl.h>
50 #define DEBUGPRINT(...) fprintf(stderr, __VA_ARGS__)
52 #define DEBUGPRINT(...) do { } while(0)
56 fdirstat_fallback(int fd
, int flags
, struct dirstat
*ds
);
60 fdirstat(int fd
, int flags
, struct dirstat
*ds
)
62 struct apfs_dir_stats_ext dstats
= {0};
63 if (flags
& DIRSTAT_FAST_ONLY
) {
64 dstats
.flags
|= APFS_DIR_STATS_FAST_PATH
;
67 int err
= ffsctl(fd
, APFSIOC_GET_DIR_STATS_EXT
, &dstats
, 0);
69 if (errno
== ENOENT
) {
70 // <rdar://problem/31696225>
76 ds
->total_size
= dstats
.total_size
;
77 ds
->descendants
= dstats
.num_children
;
84 dirstatat_np(int dfd
, const char *path
, int flags
, struct dirstat
*ds_out
, size_t ds_size
)
87 // <rdar://problem/32794924>
88 // Until APFS directory sizing is fixed, only the fallback path is
90 flags
|= DIRSTAT_FORCE_FALLBACK
;
92 // FORCE_FALLBACK trumps FAST_ONLY. Make sure to set errno accordingly in
93 // the case that a confused caller asks for both.
94 if ((flags
& (DIRSTAT_FAST_ONLY
)) && (flags
& DIRSTAT_FORCE_FALLBACK
)) {
100 int fd
= openat(dfd
, path
, O_RDONLY
| O_DIRECTORY
);
101 DEBUGPRINT("Opened %d:%s as %d\n", dfd
, path
, fd
);
102 if (fd
== -1) return -1;
104 struct dirstat ds
= {};
108 if (!(flags
& DIRSTAT_FORCE_FALLBACK
)) {
109 ret
= fdirstat(fd
, flags
, &ds
);
112 if (ret
== -1 && ((flags
& DIRSTAT_FORCE_FALLBACK
) || ((errno
== ENOTTY
) && !(flags
& DIRSTAT_FAST_ONLY
)))) {
113 ret
= fdirstat_fallback(fd
, flags
, &ds
);
116 ret
= fdirstat_fallback(fd
, flags
, &ds
);
118 int saved_errno
= errno
;
120 if (ds_size
>= sizeof(ds
)) {
121 memcpy(ds_out
, &ds
, sizeof(ds
));
123 memcpy(ds_out
, &ds
, ds_size
);
133 dirstat_np(const char *path
, int flags
, struct dirstat
*ds
, size_t ds_size
)
135 return dirstatat_np(AT_FDCWD
, path
, flags
, ds
, ds_size
);
138 #pragma mark Fallback
140 struct dirqueue_entry
{
141 STAILQ_ENTRY(dirqueue_entry
) entries
;
146 fdirstat_fallback(int parent_fd
, int flags
, struct dirstat
*ds
)
151 * This method of gathering disk usage is the fastest by far over other
152 * methods using fts or opendir/readdir + getattrlist or stat to gather
153 * information about filesystem usage. That's because this method avoids
154 * creating vnodes for each item in a directory. We implement a recursive
155 * filesystem search by appending each directory child found to a
156 * processing queue, and then process each child directory in that queue on
157 * a FIFO basis resulting in a breadth-first traversal of the filesystem.
158 * This keeps our actual implementation iterative to avoid deep filesystem
159 * hierarchies overflowing our stack.
162 dirstat_fileid_set_t fileid_seen
= _dirstat_fileid_set_create();
163 STAILQ_HEAD(, dirqueue_entry
) dirqueue_head
= STAILQ_HEAD_INITIALIZER(dirqueue_head
);
165 struct attrlist attrlist
= {
166 .bitmapcount
= ATTR_BIT_MAP_COUNT
,
167 .commonattr
= ATTR_CMN_RETURNED_ATTRS
| ATTR_CMN_ERROR
|
168 ATTR_CMN_NAME
| ATTR_CMN_OBJTYPE
| ATTR_CMN_FILEID
,
169 .dirattr
= ATTR_DIR_ENTRYCOUNT
,
170 .fileattr
= ATTR_FILE_LINKCOUNT
| ATTR_FILE_ALLOCSIZE
|
171 ATTR_FILE_DATAALLOCSIZE
,
176 * fields are in order of possible return in buffer (but note that data
177 * is packed in the actual buffer, and only relevant fields are
181 attribute_set_t returned
; //ATTR_CMN_RETURNED_ATTRS
182 uint32_t error
; //ATTR_CMN_ERROR
183 attrreference_t item_name_info
; //ATTR_CMN_NAME
184 fsobj_type_t type
; //ATTR_CMN_OBJTYPE
185 uint64_t fileid
; //ATTR_CMN_FILEID
188 u_int32_t entry_count
; //ATTR_DIR_ENTRYCOUNT
191 u_int32_t link_count
; //ATTR_FILE_LINKCOUNT
192 off_t alloc_size
; //ATTR_FILE_ALLOCSIZE
193 off_t data_alloc_size
; //ATTR_FILE_DATAALLOCSIZE
198 size_t attrbuf_len
= (32 * 1024);
199 char *attrbuf
= alloca(attrbuf_len
);
202 os_assert(!(flags
& DIRSTAT_FAST_ONLY
));
209 if (STAILQ_EMPTY(&dirqueue_head
)) {
213 struct dirqueue_entry
*dqe
= STAILQ_FIRST(&dirqueue_head
);
214 STAILQ_REMOVE_HEAD(&dirqueue_head
, entries
);
218 fd
= openat(parent_fd
, path
, O_RDONLY
| O_DIRECTORY
);
221 DEBUGPRINT( "Unable to open directory %d:%s => %s\n", parent_fd
, path
, strerror(errno
));
227 int ret_entry_count
= getattrlistbulk(fd
, &attrlist
, attrbuf
, attrbuf_len
, 0);
228 if (-1 == ret_entry_count
) {
229 if (fd
== parent_fd
) {
232 DEBUGPRINT( "getattrlistbulk on in %s returned error %s\n", path
, strerror(errno
));
234 } else if (0 == ret_entry_count
) {
237 char *cursor
= NULL
; //pointer into attrbuf
238 char *entry_start
= attrbuf
;
240 for (int index
= 0; index
< ret_entry_count
; index
++) {
241 max_attr_entry_t attrs
= {0};
244 cursor
= entry_start
;
246 memcpy(&attrs
.length
, cursor
, sizeof(attrs
.length
));
247 cursor
+= sizeof(attrs
.length
);
249 /* set starting point for next entry */
250 entry_start
+= attrs
.length
;
252 memcpy(&attrs
.returned
, cursor
, sizeof(attrs
.returned
));
253 cursor
+= sizeof(attrs
.returned
);
255 if (attrs
.returned
.commonattr
& ATTR_CMN_ERROR
) {
256 memcpy(&attrs
.error
, cursor
, sizeof(attrs
.error
));
257 cursor
+= sizeof(attrs
.error
);
261 DEBUGPRINT( "Got error %s while processing in %s\n", strerror(errno
), path
);
265 if (attrs
.returned
.commonattr
& ATTR_CMN_NAME
) {
266 memcpy(&attrs
.item_name_info
, cursor
, sizeof(attrs
.item_name_info
));
267 name
= cursor
+ attrs
.item_name_info
.attr_dataoffset
;
268 if (name
+ attrs
.item_name_info
.attr_length
> entry_start
) {
271 cursor
+= sizeof(attrs
.item_name_info
);
274 if (attrs
.returned
.commonattr
& ATTR_CMN_OBJTYPE
) {
275 memcpy(&attrs
.type
, cursor
, sizeof(attrs
.type
));
276 cursor
+= sizeof(attrs
.type
);
279 if (attrs
.returned
.commonattr
& ATTR_CMN_FILEID
) {
280 memcpy(&attrs
.fileid
, cursor
, sizeof(attrs
.fileid
));
281 cursor
+= sizeof(attrs
.fileid
);
284 if (VDIR
== attrs
.type
) {
285 if (attrs
.returned
.dirattr
& ATTR_DIR_ENTRYCOUNT
) {
286 memcpy(&attrs
.entry_count
, cursor
, sizeof(attrs
.entry_count
));
287 cursor
+= sizeof(attrs
.entry_count
);
289 // Fake it so we go down the right path below
290 attrs
.entry_count
= -1;
293 // avoid descending into empty directories
294 if (attrs
.entry_count
&& name
) {
295 struct dirqueue_entry
*dqe
= malloc(sizeof(struct dirqueue_entry
));
297 dqe
->path
= strdup(name
);
299 asprintf(&dqe
->path
, "%s/%s", path
, name
);
302 if (dqe
->path
!= NULL
) {
303 STAILQ_INSERT_TAIL(&dirqueue_head
, dqe
, entries
);
305 DEBUGPRINT( "Unable to create dqe\n");
308 } else if (attrs
.entry_count
!= 0) {
309 DEBUGPRINT( "Failed to get name for item in %s\n", path
);
310 } else if (attrs
.entry_count
== 0) {
311 // Empty directory, nothing to do
314 off_t object_size
= 0;
316 if (attrs
.returned
.fileattr
& ATTR_FILE_LINKCOUNT
) {
317 memcpy(&attrs
.link_count
, cursor
, sizeof(attrs
.link_count
));
318 cursor
+= sizeof(attrs
.link_count
);
321 if (attrs
.returned
.fileattr
& ATTR_FILE_ALLOCSIZE
) {
322 memcpy(&attrs
.alloc_size
, cursor
, sizeof(attrs
.alloc_size
));
323 cursor
+= sizeof(attrs
.alloc_size
);
324 object_size
= attrs
.alloc_size
;
327 if (attrs
.returned
.fileattr
& ATTR_FILE_DATAALLOCSIZE
) {
328 memcpy(&attrs
.data_alloc_size
, cursor
, sizeof(attrs
.data_alloc_size
));
329 cursor
+= sizeof(attrs
.data_alloc_size
);
330 if (0 == object_size
) {
331 object_size
= attrs
.data_alloc_size
;
335 if (1 == attrs
.link_count
) {
336 ds
->total_size
+= object_size
;
338 bool seen_fileid
= _dirstat_fileid_set_add(fileid_seen
, attrs
.fileid
);
340 ds
->total_size
+= object_size
;
342 DEBUGPRINT( "Skipping hardlinked file at %s/%s\n", path
, name
);
355 } while (!STAILQ_EMPTY(&dirqueue_head
));
357 _dirstat_fileid_set_destroy(fileid_seen
);