/src/wireshark/wiretap/btsnoop.c
Line | Count | Source |
1 | | /* btsnoop.c |
2 | | * |
3 | | * Wiretap Library |
4 | | * Copyright (c) 1998 by Gilbert Ramirez <gram@alumni.rice.edu> |
5 | | * |
6 | | * SPDX-License-Identifier: GPL-2.0-or-later |
7 | | */ |
8 | | |
9 | | #include "config.h" |
10 | | #include "btsnoop.h" |
11 | | |
12 | | #include <string.h> |
13 | | #include "wtap_module.h" |
14 | | #include "file_wrappers.h" |
15 | | |
16 | | /* |
17 | | * Symbian's btsnoop format is derived from Sun's snoop format. |
18 | | * See RFC 1761 for a description of the "snoop" file format. |
19 | | * See |
20 | | * |
21 | | * https://gitlab.com/wireshark/wireshark/uploads/6d44fa94c164b58516e8577f44a6ccdc/btmodified_rfc1761.txt |
22 | | * |
23 | | * for a description of the btsnoop format. |
24 | | */ |
25 | | |
26 | | /* Magic number in "btsnoop" files. */ |
27 | | static const char btsnoop_magic[] = { |
28 | | 'b', 't', 's', 'n', 'o', 'o', 'p', '\0' |
29 | | }; |
30 | | |
31 | | /* "btsnoop" file header (minus magic number). */ |
32 | | struct btsnoop_hdr { |
33 | | uint32_t version; /* version number (should be 1) */ |
34 | | uint32_t datalink; /* datalink type */ |
35 | | }; |
36 | | |
37 | | /* "btsnoop" record header. */ |
38 | | struct btsnooprec_hdr { |
39 | | uint32_t orig_len; /* actual length of packet */ |
40 | | uint32_t incl_len; /* number of octets captured in file */ |
41 | | uint32_t flags; /* packet flags */ |
42 | | uint32_t cum_drops; /* cumulative number of dropped packets */ |
43 | | int64_t ts_usec; /* timestamp microseconds */ |
44 | | }; |
45 | | |
46 | | /* H1 is unframed data with the packet type encoded in the flags field of capture header */ |
47 | | /* It can be used for any datalink by placing logging above the datalink layer of HCI */ |
48 | 0 | #define KHciLoggerDatalinkTypeH1 1001 |
49 | | /* H4 is the serial HCI with packet type encoded in the first byte of each packet */ |
50 | 0 | #define KHciLoggerDatalinkTypeH4 1002 |
51 | | /* CSR's PPP derived bluecore serial protocol - in practice we log in H1 format after deframing */ |
52 | 0 | #define KHciLoggerDatalinkTypeBCSP 1003 |
53 | | /* H5 is the official three wire serial protocol derived from BCSP*/ |
54 | 0 | #define KHciLoggerDatalinkTypeH5 1004 |
55 | | /* Linux Monitor */ |
56 | 0 | #define KHciLoggerDatalinkLinuxMonitor 2001 |
57 | | /* BlueZ 5 Simulator */ |
58 | 0 | #define KHciLoggerDatalinkBlueZ5Simulator 2002 |
59 | | |
60 | 0 | #define KHciLoggerHostToController 0 |
61 | 0 | #define KHciLoggerControllerToHost 0x00000001 |
62 | 0 | #define KHciLoggerACLDataFrame 0 |
63 | 0 | #define KHciLoggerCommandOrEvent 0x00000002 |
64 | | |
65 | | static const int64_t KUnixTimeBase = INT64_C(0x00dcddb30f2f8000); /* offset from symbian - unix time */ |
66 | | |
67 | | static bool btsnoop_read(wtap *wth, wtap_rec *rec, |
68 | | int *err, char **err_info, int64_t *offset); |
69 | | static bool btsnoop_seek_read(wtap *wth, int64_t seek_off, |
70 | | wtap_rec *rec, int *err, char **err_info); |
71 | | static bool btsnoop_read_record(wtap *wth, FILE_T fh, |
72 | | wtap_rec *rec, int *err, char **err_info); |
73 | | |
74 | | static int btsnoop_file_type_subtype = -1; |
75 | | |
76 | | void register_btsnoop(void); |
77 | | |
78 | | wtap_open_return_val btsnoop_open(wtap *wth, int *err, char **err_info) |
79 | 0 | { |
80 | 0 | char magic[sizeof btsnoop_magic]; |
81 | 0 | struct btsnoop_hdr hdr; |
82 | |
|
83 | 0 | int file_encap=WTAP_ENCAP_UNKNOWN; |
84 | | |
85 | | /* Read in the string that should be at the start of a "btsnoop" file */ |
86 | 0 | if (!wtap_read_bytes(wth->fh, magic, sizeof magic, err, err_info)) { |
87 | 0 | if (*err != WTAP_ERR_SHORT_READ) |
88 | 0 | return WTAP_OPEN_ERROR; |
89 | 0 | return WTAP_OPEN_NOT_MINE; |
90 | 0 | } |
91 | | |
92 | 0 | if (memcmp(magic, btsnoop_magic, sizeof btsnoop_magic) != 0) { |
93 | 0 | return WTAP_OPEN_NOT_MINE; |
94 | 0 | } |
95 | | |
96 | | /* Read the rest of the header. */ |
97 | 0 | if (!wtap_read_bytes(wth->fh, &hdr, sizeof hdr, err, err_info)) |
98 | 0 | return WTAP_OPEN_ERROR; |
99 | | |
100 | | /* |
101 | | * Make sure it's a version we support. |
102 | | */ |
103 | 0 | hdr.version = g_ntohl(hdr.version); |
104 | 0 | if (hdr.version != 1) { |
105 | 0 | *err = WTAP_ERR_UNSUPPORTED; |
106 | 0 | *err_info = ws_strdup_printf("btsnoop: version %u unsupported", hdr.version); |
107 | 0 | return WTAP_OPEN_ERROR; |
108 | 0 | } |
109 | | |
110 | 0 | hdr.datalink = g_ntohl(hdr.datalink); |
111 | 0 | switch (hdr.datalink) { |
112 | 0 | case KHciLoggerDatalinkTypeH1: |
113 | 0 | file_encap=WTAP_ENCAP_BLUETOOTH_HCI; |
114 | 0 | break; |
115 | 0 | case KHciLoggerDatalinkTypeH4: |
116 | 0 | file_encap=WTAP_ENCAP_BLUETOOTH_H4_WITH_PHDR; |
117 | 0 | break; |
118 | 0 | case KHciLoggerDatalinkTypeBCSP: |
119 | 0 | *err = WTAP_ERR_UNSUPPORTED; |
120 | 0 | *err_info = g_strdup("btsnoop: BCSP capture logs unsupported"); |
121 | 0 | return WTAP_OPEN_ERROR; |
122 | 0 | case KHciLoggerDatalinkTypeH5: |
123 | 0 | *err = WTAP_ERR_UNSUPPORTED; |
124 | 0 | *err_info = g_strdup("btsnoop: H5 capture logs unsupported"); |
125 | 0 | return WTAP_OPEN_ERROR; |
126 | 0 | case KHciLoggerDatalinkLinuxMonitor: |
127 | 0 | file_encap=WTAP_ENCAP_BLUETOOTH_LINUX_MONITOR; |
128 | 0 | break; |
129 | 0 | case KHciLoggerDatalinkBlueZ5Simulator: |
130 | 0 | *err = WTAP_ERR_UNSUPPORTED; |
131 | 0 | *err_info = g_strdup("btsnoop: BlueZ 5 Simulator capture logs unsupported"); |
132 | 0 | return WTAP_OPEN_ERROR; |
133 | 0 | default: |
134 | 0 | *err = WTAP_ERR_UNSUPPORTED; |
135 | 0 | *err_info = ws_strdup_printf("btsnoop: datalink type %u unknown or unsupported", hdr.datalink); |
136 | 0 | return WTAP_OPEN_ERROR; |
137 | 0 | } |
138 | | |
139 | 0 | wth->subtype_read = btsnoop_read; |
140 | 0 | wth->subtype_seek_read = btsnoop_seek_read; |
141 | 0 | wth->file_encap = file_encap; |
142 | 0 | wth->snapshot_length = 0; /* not available in header */ |
143 | 0 | wth->file_tsprec = WTAP_TSPREC_USEC; |
144 | 0 | wth->file_type_subtype = btsnoop_file_type_subtype; |
145 | | |
146 | | /* |
147 | | * Add an IDB; we don't know how many interfaces were |
148 | | * involved, so we just say one interface, about which |
149 | | * we only know the link-layer type, snapshot length, |
150 | | * and time stamp resolution. |
151 | | */ |
152 | 0 | wtap_add_generated_idb(wth); |
153 | |
|
154 | 0 | return WTAP_OPEN_MINE; |
155 | 0 | } |
156 | | |
157 | | static bool btsnoop_read(wtap *wth, wtap_rec *rec, |
158 | | int *err, char **err_info, int64_t *offset) |
159 | 0 | { |
160 | 0 | *offset = file_tell(wth->fh); |
161 | |
|
162 | 0 | return btsnoop_read_record(wth, wth->fh, rec, err, err_info); |
163 | 0 | } |
164 | | |
165 | | static bool btsnoop_seek_read(wtap *wth, int64_t seek_off, |
166 | | wtap_rec *rec, int *err, char **err_info) |
167 | 0 | { |
168 | 0 | if (file_seek(wth->random_fh, seek_off, SEEK_SET, err) == -1) |
169 | 0 | return false; |
170 | | |
171 | 0 | return btsnoop_read_record(wth, wth->random_fh, rec, err, err_info); |
172 | 0 | } |
173 | | |
174 | | static bool btsnoop_read_record(wtap *wth, FILE_T fh, |
175 | | wtap_rec *rec, int *err, char **err_info) |
176 | 0 | { |
177 | 0 | struct btsnooprec_hdr hdr; |
178 | 0 | uint32_t packet_size; |
179 | 0 | uint32_t flags; |
180 | 0 | uint32_t orig_size; |
181 | 0 | int64_t ts; |
182 | | |
183 | | /* Read record header. */ |
184 | |
|
185 | 0 | if (!wtap_read_bytes_or_eof(fh, &hdr, sizeof hdr, err, err_info)) |
186 | 0 | return false; |
187 | | |
188 | 0 | packet_size = g_ntohl(hdr.incl_len); |
189 | 0 | orig_size = g_ntohl(hdr.orig_len); |
190 | 0 | flags = g_ntohl(hdr.flags); |
191 | 0 | if (packet_size > WTAP_MAX_PACKET_SIZE_STANDARD) { |
192 | | /* |
193 | | * Probably a corrupt capture file; don't blow up trying |
194 | | * to allocate space for an immensely-large packet. |
195 | | */ |
196 | 0 | *err = WTAP_ERR_BAD_FILE; |
197 | 0 | *err_info = ws_strdup_printf("btsnoop: File has %u-byte packet, bigger than maximum of %u", |
198 | 0 | packet_size, WTAP_MAX_PACKET_SIZE_STANDARD); |
199 | 0 | return false; |
200 | 0 | } |
201 | | |
202 | 0 | ts = GINT64_FROM_BE(hdr.ts_usec); |
203 | 0 | ts -= KUnixTimeBase; |
204 | |
|
205 | 0 | wtap_setup_packet_rec(rec, wth->file_encap); |
206 | 0 | rec->block = wtap_block_create(WTAP_BLOCK_PACKET); |
207 | 0 | rec->presence_flags = WTAP_HAS_TS|WTAP_HAS_CAP_LEN; |
208 | 0 | rec->ts.secs = (unsigned)(ts / 1000000); |
209 | 0 | rec->ts.nsecs = (unsigned)((ts % 1000000) * 1000); |
210 | 0 | rec->rec_header.packet_header.caplen = packet_size; |
211 | 0 | rec->rec_header.packet_header.len = orig_size; |
212 | 0 | if(wth->file_encap == WTAP_ENCAP_BLUETOOTH_H4_WITH_PHDR) |
213 | 0 | { |
214 | 0 | rec->rec_header.packet_header.pseudo_header.p2p.sent = (flags & KHciLoggerControllerToHost) ? false : true; |
215 | 0 | } else if(wth->file_encap == WTAP_ENCAP_BLUETOOTH_HCI) { |
216 | 0 | rec->rec_header.packet_header.pseudo_header.bthci.sent = (flags & KHciLoggerControllerToHost) ? false : true; |
217 | 0 | if(flags & KHciLoggerCommandOrEvent) |
218 | 0 | { |
219 | 0 | if(rec->rec_header.packet_header.pseudo_header.bthci.sent) |
220 | 0 | { |
221 | 0 | rec->rec_header.packet_header.pseudo_header.bthci.channel = BTHCI_CHANNEL_COMMAND; |
222 | 0 | } |
223 | 0 | else |
224 | 0 | { |
225 | 0 | rec->rec_header.packet_header.pseudo_header.bthci.channel = BTHCI_CHANNEL_EVENT; |
226 | 0 | } |
227 | 0 | } |
228 | 0 | else |
229 | 0 | { |
230 | 0 | rec->rec_header.packet_header.pseudo_header.bthci.channel = BTHCI_CHANNEL_ACL; |
231 | 0 | } |
232 | 0 | } else if (wth->file_encap == WTAP_ENCAP_BLUETOOTH_LINUX_MONITOR) { |
233 | 0 | rec->rec_header.packet_header.pseudo_header.btmon.opcode = flags & 0xFFFF; |
234 | 0 | rec->rec_header.packet_header.pseudo_header.btmon.adapter_id = flags >> 16; |
235 | 0 | } |
236 | | |
237 | | |
238 | | /* Read packet data. */ |
239 | 0 | return wtap_read_bytes_buffer(fh, &rec->data, |
240 | 0 | rec->rec_header.packet_header.caplen, |
241 | 0 | err, err_info); |
242 | 0 | } |
243 | | |
244 | | /* Returns 0 if we could write the specified encapsulation type, |
245 | | an error indication otherwise. */ |
246 | | static int btsnoop_dump_can_write_encap(int encap) |
247 | 0 | { |
248 | | /* Per-packet encapsulations aren't supported. */ |
249 | 0 | if (encap == WTAP_ENCAP_PER_PACKET) |
250 | 0 | return WTAP_ERR_ENCAP_PER_PACKET_UNSUPPORTED; |
251 | | |
252 | | /* |
253 | | * XXX - for now we only support WTAP_ENCAP_BLUETOOTH_HCI, |
254 | | * WTAP_ENCAP_BLUETOOTH_H4_WITH_PHDR, and |
255 | | * WTAP_ENCAP_BLUETOOTH_LINUX_MONITOR. |
256 | | */ |
257 | 0 | if (encap != WTAP_ENCAP_BLUETOOTH_HCI && |
258 | 0 | encap != WTAP_ENCAP_BLUETOOTH_H4_WITH_PHDR && |
259 | 0 | encap != WTAP_ENCAP_BLUETOOTH_LINUX_MONITOR) |
260 | 0 | return WTAP_ERR_UNWRITABLE_ENCAP; |
261 | | |
262 | 0 | return 0; |
263 | 0 | } |
264 | | |
265 | | static bool btsnoop_dump(wtap_dumper *wdh, const wtap_rec *rec, |
266 | | int *err, char **err_info) |
267 | 0 | { |
268 | 0 | const union wtap_pseudo_header *pseudo_header = &rec->rec_header.packet_header.pseudo_header; |
269 | 0 | const uint8_t *pd; |
270 | 0 | struct btsnooprec_hdr rec_hdr; |
271 | 0 | uint32_t flags; |
272 | 0 | int64_t nsecs; |
273 | 0 | int64_t ts_usec; |
274 | | |
275 | | /* We can only write packet records. */ |
276 | 0 | if (rec->rec_type != REC_TYPE_PACKET) { |
277 | 0 | *err = WTAP_ERR_UNWRITABLE_REC_TYPE; |
278 | 0 | *err_info = wtap_unwritable_rec_type_err_string(rec); |
279 | 0 | return false; |
280 | 0 | } |
281 | | |
282 | | /* |
283 | | * Make sure this packet doesn't have a link-layer type that |
284 | | * differs from the one for the file. |
285 | | */ |
286 | 0 | if (wdh->file_encap != rec->rec_header.packet_header.pkt_encap) { |
287 | 0 | *err = WTAP_ERR_ENCAP_PER_PACKET_UNSUPPORTED; |
288 | 0 | return false; |
289 | 0 | } |
290 | | |
291 | | /* Don't write out anything bigger than we can read. */ |
292 | 0 | if (rec->rec_header.packet_header.caplen > WTAP_MAX_PACKET_SIZE_STANDARD) { |
293 | 0 | *err = WTAP_ERR_PACKET_TOO_LARGE; |
294 | 0 | return false; |
295 | 0 | } |
296 | | |
297 | 0 | rec_hdr.incl_len = GUINT32_TO_BE(rec->rec_header.packet_header.caplen); |
298 | 0 | rec_hdr.orig_len = GUINT32_TO_BE(rec->rec_header.packet_header.len); |
299 | |
|
300 | 0 | pd = ws_buffer_start_ptr(&rec->data); |
301 | |
|
302 | 0 | switch (wdh->file_encap) { |
303 | | |
304 | 0 | case WTAP_ENCAP_BLUETOOTH_HCI: |
305 | 0 | switch (pseudo_header->bthci.channel) { |
306 | | |
307 | 0 | case BTHCI_CHANNEL_COMMAND: |
308 | 0 | if (!pseudo_header->bthci.sent) { |
309 | 0 | *err = WTAP_ERR_UNWRITABLE_REC_DATA; |
310 | 0 | *err_info = ws_strdup_printf("btsnoop: Command channel, sent false"); |
311 | 0 | return false; |
312 | 0 | } |
313 | 0 | flags = KHciLoggerCommandOrEvent|KHciLoggerHostToController; |
314 | 0 | break; |
315 | | |
316 | 0 | case BTHCI_CHANNEL_EVENT: |
317 | 0 | if (pseudo_header->bthci.sent) { |
318 | 0 | *err = WTAP_ERR_UNWRITABLE_REC_DATA; |
319 | 0 | *err_info = ws_strdup_printf("btsnoop: Event channel, sent true"); |
320 | 0 | return false; |
321 | 0 | } |
322 | 0 | flags = KHciLoggerCommandOrEvent|KHciLoggerControllerToHost; |
323 | 0 | break; |
324 | | |
325 | 0 | case BTHCI_CHANNEL_ACL: |
326 | 0 | if (pseudo_header->bthci.sent) |
327 | 0 | flags = KHciLoggerACLDataFrame|KHciLoggerHostToController; |
328 | 0 | else |
329 | 0 | flags = KHciLoggerACLDataFrame|KHciLoggerControllerToHost; |
330 | 0 | break; |
331 | | |
332 | 0 | default: |
333 | 0 | *err = WTAP_ERR_UNWRITABLE_REC_DATA; |
334 | 0 | *err_info = ws_strdup_printf("btsnoop: Unknown channel %u", |
335 | 0 | pseudo_header->bthci.channel); |
336 | 0 | return false; |
337 | 0 | } |
338 | 0 | break; |
339 | | |
340 | 0 | case WTAP_ENCAP_BLUETOOTH_H4_WITH_PHDR: |
341 | 0 | if (pseudo_header->p2p.sent) |
342 | 0 | flags = KHciLoggerHostToController; |
343 | 0 | else |
344 | 0 | flags = KHciLoggerControllerToHost; |
345 | 0 | if (rec->rec_header.packet_header.caplen >= 1 && |
346 | 0 | (pd[0] == 0x01 || pd[0] == 0x04)) |
347 | 0 | flags |= KHciLoggerCommandOrEvent; |
348 | 0 | break; |
349 | | |
350 | 0 | case WTAP_ENCAP_BLUETOOTH_LINUX_MONITOR: |
351 | 0 | flags = (pseudo_header->btmon.adapter_id << 16) | pseudo_header->btmon.opcode; |
352 | 0 | break; |
353 | | |
354 | 0 | default: |
355 | | /* We should never get here - our open routine should only get |
356 | | called for the types above. */ |
357 | 0 | *err = WTAP_ERR_INTERNAL; |
358 | 0 | *err_info = ws_strdup_printf("btsnoop: invalid encapsulation %u", |
359 | 0 | wdh->file_encap); |
360 | 0 | return false; |
361 | 0 | } |
362 | 0 | rec_hdr.flags = GUINT32_TO_BE(flags); |
363 | 0 | rec_hdr.cum_drops = GUINT32_TO_BE(0); |
364 | |
|
365 | 0 | nsecs = rec->ts.nsecs; |
366 | 0 | ts_usec = ((int64_t) rec->ts.secs * 1000000) + (nsecs / 1000); |
367 | 0 | ts_usec += KUnixTimeBase; |
368 | 0 | rec_hdr.ts_usec = GINT64_TO_BE(ts_usec); |
369 | |
|
370 | 0 | if (!wtap_dump_file_write(wdh, &rec_hdr, sizeof rec_hdr, err)) |
371 | 0 | return false; |
372 | 0 | if (!wtap_dump_file_write(wdh, pd, rec->rec_header.packet_header.caplen, err)) |
373 | 0 | return false; |
374 | 0 | return true; |
375 | 0 | } |
376 | | |
377 | | /* Returns true on success, false on failure; sets "*err" to an error code on |
378 | | failure */ |
379 | | static bool btsnoop_dump_open(wtap_dumper *wdh, int *err, char **err_info _U_) |
380 | 0 | { |
381 | 0 | struct btsnoop_hdr file_hdr; |
382 | 0 | uint32_t datalink; |
383 | | |
384 | | /* This is a btsnoop file */ |
385 | 0 | wdh->subtype_write = btsnoop_dump; |
386 | |
|
387 | 0 | switch (wdh->file_encap) { |
388 | | |
389 | 0 | case WTAP_ENCAP_BLUETOOTH_HCI: |
390 | 0 | datalink = KHciLoggerDatalinkTypeH1; |
391 | 0 | break; |
392 | | |
393 | 0 | case WTAP_ENCAP_BLUETOOTH_H4_WITH_PHDR: |
394 | 0 | datalink = KHciLoggerDatalinkTypeH4; |
395 | 0 | break; |
396 | | |
397 | 0 | case WTAP_ENCAP_BLUETOOTH_LINUX_MONITOR: |
398 | 0 | datalink = KHciLoggerDatalinkLinuxMonitor; |
399 | 0 | break; |
400 | | |
401 | 0 | default: |
402 | | /* We should never get here - our open routine should only get |
403 | | called for the types above. */ |
404 | 0 | *err = WTAP_ERR_INTERNAL; |
405 | 0 | *err_info = ws_strdup_printf("btsnoop: invalid encapsulation %u", |
406 | 0 | wdh->file_encap); |
407 | 0 | return false; |
408 | 0 | } |
409 | | |
410 | | /* Write the file header. */ |
411 | 0 | if (!wtap_dump_file_write(wdh, btsnoop_magic, sizeof btsnoop_magic, err)) |
412 | 0 | return false; |
413 | | |
414 | | /* current "btsnoop" format is 1 */ |
415 | 0 | file_hdr.version = GUINT32_TO_BE(1); |
416 | | /* HCI type encoded in first byte */ |
417 | 0 | file_hdr.datalink = GUINT32_TO_BE(datalink); |
418 | |
|
419 | 0 | if (!wtap_dump_file_write(wdh, &file_hdr, sizeof file_hdr, err)) |
420 | 0 | return false; |
421 | | |
422 | 0 | return true; |
423 | 0 | } |
424 | | |
425 | | static const struct supported_block_type btsnoop_blocks_supported[] = { |
426 | | /* |
427 | | * We support packet blocks, with no comments or other options. |
428 | | */ |
429 | | { WTAP_BLOCK_PACKET, MULTIPLE_BLOCKS_SUPPORTED, NO_OPTIONS_SUPPORTED } |
430 | | }; |
431 | | |
432 | | static const struct file_type_subtype_info btsnoop_info = { |
433 | | "Symbian OS btsnoop", "btsnoop", "log", NULL, |
434 | | false, BLOCKS_SUPPORTED(btsnoop_blocks_supported), |
435 | | btsnoop_dump_can_write_encap, btsnoop_dump_open, NULL |
436 | | }; |
437 | | |
438 | | void register_btsnoop(void) |
439 | 14 | { |
440 | 14 | btsnoop_file_type_subtype = wtap_register_file_type_subtype(&btsnoop_info); |
441 | | |
442 | | /* |
443 | | * Register name for backwards compatibility with the |
444 | | * wtap_filetypes table in Lua. |
445 | | */ |
446 | 14 | wtap_register_backwards_compatibility_lua_name("BTSNOOP", |
447 | 14 | btsnoop_file_type_subtype); |
448 | 14 | } |
449 | | |
450 | | /* |
451 | | * Editor modelines - https://www.wireshark.org/tools/modelines.html |
452 | | * |
453 | | * Local variables: |
454 | | * c-basic-offset: 4 |
455 | | * tab-width: 8 |
456 | | * indent-tabs-mode: nil |
457 | | * End: |
458 | | * |
459 | | * vi: set shiftwidth=4 tabstop=8 expandtab: |
460 | | * :indentSize=4:tabSize=8:noTabs=true: |
461 | | */ |