9d03445bbbadd2a4b9c9e272623728fa7cc9ca81
[project/luci.git] / libs / sys / luasrc / sys.lua
1 --[[
2 LuCI - System library
3
4 Description:
5 Utilities for interaction with the Linux system
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27
28 local io = require "io"
29 local os = require "os"
30 local nixio = require "nixio"
31 local table = require "table"
32
33 local luci = {}
34 luci.util = require "luci.util"
35 luci.fs = require "luci.fs"
36 luci.ip = require "luci.ip"
37
38 local tonumber, ipairs, pairs, pcall, type, next =
39 tonumber, ipairs, pairs, pcall, type, next
40
41
42 --- LuCI Linux and POSIX system utilities.
43 module "luci.sys"
44
45 --- Execute a given shell command and return the error code
46 -- @class function
47 -- @name call
48 -- @param ... Command to call
49 -- @return Error code of the command
50 function call(...)
51 return os.execute(...) / 256
52 end
53
54 --- Execute a given shell command and capture its standard output
55 -- @class function
56 -- @name exec
57 -- @param command Command to call
58 -- @return String containg the return the output of the command
59 exec = luci.util.exec
60
61 --- Invoke the luci-flash executable to write an image to the flash memory.
62 -- @param image Local path or URL to image file
63 -- @param kpattern Pattern of files to keep over flash process
64 -- @return Return value of os.execute()
65 function flash(image, kpattern)
66 local cmd = "luci-flash "
67 if kpattern then
68 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
69 end
70 cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
71
72 return os.execute(cmd)
73 end
74
75 --- Retrieve information about currently mounted file systems.
76 -- @return Table containing mount information
77 function mounts()
78 local data = {}
79 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
80 local ps = luci.util.execi("df")
81
82 if not ps then
83 return
84 else
85 ps()
86 end
87
88 for line in ps do
89 local row = {}
90
91 local j = 1
92 for value in line:gmatch("[^%s]+") do
93 row[k[j]] = value
94 j = j + 1
95 end
96
97 if row[k[1]] then
98
99 -- this is a rather ugly workaround to cope with wrapped lines in
100 -- the df output:
101 --
102 -- /dev/scsi/host0/bus0/target0/lun0/part3
103 -- 114382024 93566472 15005244 86% /mnt/usb
104 --
105
106 if not row[k[2]] then
107 j = 2
108 line = ps()
109 for value in line:gmatch("[^%s]+") do
110 row[k[j]] = value
111 j = j + 1
112 end
113 end
114
115 table.insert(data, row)
116 end
117 end
118
119 return data
120 end
121
122 --- Retrieve environment variables. If no variable is given then a table
123 -- containing the whole environment is returned otherwise this function returns
124 -- the corresponding string value for the given name or nil if no such variable
125 -- exists.
126 -- @class function
127 -- @name getenv
128 -- @param var Name of the environment variable to retrieve (optional)
129 -- @return String containg the value of the specified variable
130 -- @return Table containing all variables if no variable name is given
131 getenv = nixio.getenv
132
133 --- Get or set the current hostname.
134 -- @param String containing a new hostname to set (optional)
135 -- @return String containing the system hostname
136 function hostname(newname)
137 if type(newname) == "string" and #newname > 0 then
138 luci.fs.writefile( "/proc/sys/kernel/hostname", newname .. "\n" )
139 return newname
140 else
141 return nixio.uname().nodename
142 end
143 end
144
145 --- Returns the contents of a documented referred by an URL.
146 -- @param url The URL to retrieve
147 -- @param stream Return a stream instead of a buffer
148 -- @param target Directly write to target file name
149 -- @return String containing the contents of given the URL
150 function httpget(url, stream, target)
151 if not target then
152 local source = stream and io.popen or luci.util.exec
153 return source("wget -qO- '"..url:gsub("'", "").."'")
154 else
155 return os.execute("wget -qO '%s' '%s'" %
156 {target:gsub("'", ""), url:gsub("'", "")})
157 end
158 end
159
160 --- Returns the system load average values.
161 -- @return String containing the average load value 1 minute ago
162 -- @return String containing the average load value 5 minutes ago
163 -- @return String containing the average load value 15 minutes ago
164 function loadavg()
165 local info = nixio.sysinfo()
166 return info.loads[1], info.loads[2], info.loads[3]
167 end
168
169 --- Initiate a system reboot.
170 -- @return Return value of os.execute()
171 function reboot()
172 return os.execute("reboot >/dev/null 2>&1")
173 end
174
175 --- Returns the system type, cpu name and installed physical memory.
176 -- @return String containing the system or platform identifier
177 -- @return String containing hardware model information
178 -- @return String containing the total memory amount in kB
179 -- @return String containing the memory used for caching in kB
180 -- @return String containing the memory used for buffering in kB
181 -- @return String containing the free memory amount in kB
182 function sysinfo()
183 local cpuinfo = luci.fs.readfile("/proc/cpuinfo")
184 local meminfo = luci.fs.readfile("/proc/meminfo")
185
186 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
187 local model = ""
188 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
189 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
190 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
191 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
192
193 if not system then
194 system = nixio.uname().machine
195 model = cpuinfo:match("model name.-:%s*([^\n]+)")
196 if not model then
197 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
198 end
199 else
200 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
201 end
202
203 return system, model, memtotal, memcached, membuffers, memfree
204 end
205
206 --- Retrieves the output of the "logread" command.
207 -- @return String containing the current log buffer
208 function syslog()
209 return luci.util.exec("logread")
210 end
211
212 --- Retrieves the output of the "dmesg" command.
213 -- @return String containing the current log buffer
214 function dmesg()
215 return luci.util.exec("dmesg")
216 end
217
218 --- Generates a random id with specified length.
219 -- @param bytes Number of bytes for the unique id
220 -- @return String containing hex encoded id
221 function uniqueid(bytes)
222 local rand = luci.fs.readfile("/dev/urandom", bytes)
223 return rand and nixio.bin.hexlify(rand)
224 end
225
226 --- Returns the current system uptime stats.
227 -- @return String containing total uptime in seconds
228 function uptime()
229 return nixio.sysinfo().uptime
230 end
231
232
233 --- LuCI system utilities / network related functions.
234 -- @class module
235 -- @name luci.sys.net
236 net = {}
237
238 --- Returns the current arp-table entries as two-dimensional table.
239 -- @return Table of table containing the current arp entries.
240 -- The following fields are defined for arp entry objects:
241 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
242 function net.arptable(callback)
243 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
244 end
245
246 --- Returns conntrack information
247 -- @return Table with the currently tracked IP connections
248 function net.conntrack(callback)
249 local connt = {}
250 if luci.fs.access("/proc/net/nf_conntrack", "r") then
251 for line in io.lines("/proc/net/nf_conntrack") do
252 line = line:match "^(.-( [^ =]+=).-)%2"
253 local entry, flags = _parse_mixed_record(line, " +")
254 entry.layer3 = flags[1]
255 entry.layer4 = flags[3]
256 for i=1, #entry do
257 entry[i] = nil
258 end
259
260 if callback then
261 callback(entry)
262 else
263 connt[#connt+1] = entry
264 end
265 end
266 elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
267 for line in io.lines("/proc/net/ip_conntrack") do
268 line = line:match "^(.-( [^ =]+=).-)%2"
269 local entry, flags = _parse_mixed_record(line, " +")
270 entry.layer3 = "ipv4"
271 entry.layer4 = flags[1]
272 for i=1, #entry do
273 entry[i] = nil
274 end
275
276 if callback then
277 callback(entry)
278 else
279 connt[#connt+1] = entry
280 end
281 end
282 else
283 return nil
284 end
285 return connt
286 end
287
288 --- Determine the current IPv4 default route. If multiple default routes exist,
289 -- return the one with the lowest metric.
290 -- @return Table with the properties of the current default route.
291 -- The following fields are defined:
292 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
293 -- "flags", "device" }
294 function net.defaultroute()
295 local route
296
297 net.routes(function(rt)
298 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
299 route = rt
300 end
301 end)
302
303 return route
304 end
305
306 --- Determine the current IPv6 default route. If multiple default routes exist,
307 -- return the one with the lowest metric.
308 -- @return Table with the properties of the current default route.
309 -- The following fields are defined:
310 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
311 -- "flags", "device" }
312 function net.defaultroute6()
313 local route
314
315 net.routes6(function(rt)
316 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
317 route = rt
318 end
319 end)
320
321 return route
322 end
323
324 --- Determine the names of available network interfaces.
325 -- @return Table containing all current interface names
326 function net.devices()
327 local devs = {}
328 for k, v in ipairs(nixio.getifaddrs()) do
329 if v.family == "packet" then
330 devs[#devs+1] = v.name
331 end
332 end
333 return devs
334 end
335
336
337 --- Return information about available network interfaces.
338 -- @return Table containing all current interface names and their information
339 function net.deviceinfo()
340 local devs = {}
341 for k, v in ipairs(nixio.getifaddrs()) do
342 if v.family == "packet" then
343 local d = v.data
344 d[1] = d.rx_bytes
345 d[2] = d.rx_packets
346 d[3] = d.rx_errors
347 d[4] = d.rx_dropped
348 d[5] = 0
349 d[6] = 0
350 d[7] = 0
351 d[8] = d.multicast
352 d[9] = d.tx_bytes
353 d[10] = d.tx_packets
354 d[11] = d.tx_errors
355 d[12] = d.tx_dropped
356 d[13] = 0
357 d[14] = d.collisions
358 d[15] = 0
359 d[16] = 0
360 devs[v.name] = d
361 end
362 end
363 return devs
364 end
365
366
367 -- Determine the MAC address belonging to the given IP address.
368 -- @param ip IPv4 address
369 -- @return String containing the MAC address or nil if it cannot be found
370 function net.ip4mac(ip)
371 local mac = nil
372
373 for i, l in ipairs(net.arptable()) do
374 if l["IP address"] == ip then
375 mac = l["HW address"]
376 end
377 end
378
379 return mac
380 end
381
382 --- Returns the current kernel routing table entries.
383 -- @return Table of tables with properties of the corresponding routes.
384 -- The following fields are defined for route entry tables:
385 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
386 -- "flags", "device" }
387 function net.routes(callback)
388 local routes = { }
389
390 for line in io.lines("/proc/net/route") do
391
392 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
393 dst_mask, mtu, win, irtt = line:match(
394 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
395 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
396 )
397
398 if dev then
399 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
400 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
401 dst_ip = luci.ip.Hex(
402 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
403 )
404
405 local rt = {
406 dest = dst_ip,
407 gateway = gateway,
408 metric = tonumber(metric),
409 refcount = tonumber(refcnt),
410 usecount = tonumber(usecnt),
411 mtu = tonumber(mtu),
412 window = tonumber(window),
413 irtt = tonumber(irtt),
414 flags = tonumber(flags, 16),
415 device = dev
416 }
417
418 if callback then
419 callback(rt)
420 else
421 routes[#routes+1] = rt
422 end
423 end
424 end
425
426 return routes
427 end
428
429 --- Returns the current ipv6 kernel routing table entries.
430 -- @return Table of tables with properties of the corresponding routes.
431 -- The following fields are defined for route entry tables:
432 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
433 -- "flags", "device" }
434 function net.routes6(callback)
435 if luci.fs.access("/proc/net/ipv6_route", "r") then
436 local routes = { }
437
438 for line in io.lines("/proc/net/ipv6_route") do
439
440 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
441 metric, refcnt, usecnt, flags, dev = line:match(
442 "([a-f0-9]+) ([a-f0-9]+) " ..
443 "([a-f0-9]+) ([a-f0-9]+) " ..
444 "([a-f0-9]+) ([a-f0-9]+) " ..
445 "([a-f0-9]+) ([a-f0-9]+) " ..
446 "([a-f0-9]+) +([^%s]+)"
447 )
448
449 src_ip = luci.ip.Hex(
450 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
451 )
452
453 dst_ip = luci.ip.Hex(
454 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
455 )
456
457 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
458
459 local rt = {
460 source = src_ip,
461 dest = dst_ip,
462 nexthop = nexthop,
463 metric = tonumber(metric, 16),
464 refcount = tonumber(refcnt, 16),
465 usecount = tonumber(usecnt, 16),
466 flags = tonumber(flags, 16),
467 device = dev
468 }
469
470 if callback then
471 callback(rt)
472 else
473 routes[#routes+1] = rt
474 end
475 end
476
477 return routes
478 end
479 end
480
481 --- Tests whether the given host responds to ping probes.
482 -- @param host String containing a hostname or IPv4 address
483 -- @return Number containing 0 on success and >= 1 on error
484 function net.pingtest(host)
485 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
486 end
487
488
489 --- LuCI system utilities / process related functions.
490 -- @class module
491 -- @name luci.sys.process
492 process = {}
493
494 --- Get the current process id.
495 -- @class function
496 -- @name process.info
497 -- @return Number containing the current pid
498 function process.info(key)
499 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
500 return not key and s or s[key]
501 end
502
503 --- Retrieve information about currently running processes.
504 -- @return Table containing process information
505 function process.list()
506 local data = {}
507 local k
508 local ps = luci.util.execi("top -bn1")
509
510 if not ps then
511 return
512 end
513
514 while true do
515 local line = ps()
516 if not line then
517 return
518 end
519
520 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
521 if k[1] == "PID" then
522 break
523 end
524 end
525
526 for line in ps do
527 local row = {}
528
529 line = luci.util.trim(line)
530 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
531 row[k[i]] = value
532 end
533
534 local pid = tonumber(row[k[1]])
535 if pid then
536 data[pid] = row
537 end
538 end
539
540 return data
541 end
542
543 --- Set the gid of a process identified by given pid.
544 -- @param gid Number containing the Unix group id
545 -- @return Boolean indicating successful operation
546 -- @return String containing the error message if failed
547 -- @return Number containing the error code if failed
548 function process.setgroup(gid)
549 return nixio.setgid(gid)
550 end
551
552 --- Set the uid of a process identified by given pid.
553 -- @param uid Number containing the Unix user id
554 -- @return Boolean indicating successful operation
555 -- @return String containing the error message if failed
556 -- @return Number containing the error code if failed
557 function process.setuser(uid)
558 return nixio.setuid(uid)
559 end
560
561 --- Send a signal to a process identified by given pid.
562 -- @class function
563 -- @name process.signal
564 -- @param pid Number containing the process id
565 -- @param sig Signal to send (default: 15 [SIGTERM])
566 -- @return Boolean indicating successful operation
567 -- @return Number containing the error code if failed
568 process.signal = nixio.kill
569
570
571 --- LuCI system utilities / user related functions.
572 -- @class module
573 -- @name luci.sys.user
574 user = {}
575
576 --- Retrieve user informations for given uid.
577 -- @class function
578 -- @name getuser
579 -- @param uid Number containing the Unix user id
580 -- @return Table containing the following fields:
581 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
582 user.getuser = nixio.getpw
583
584 --- Test whether given string matches the password of a given system user.
585 -- @param username String containing the Unix user name
586 -- @param pass String containing the password to compare
587 -- @return Boolean indicating wheather the passwords are equal
588 function user.checkpasswd(username, pass)
589 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
590 local pwh = pwe and (pwe.pwdp or pwe.passwd)
591 if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
592 return false
593 else
594 return true
595 end
596 end
597
598 --- Change the password of given user.
599 -- @param username String containing the Unix user name
600 -- @param password String containing the password to compare
601 -- @return Number containing 0 on success and >= 1 on error
602 function user.setpasswd(username, password)
603 if password then
604 password = password:gsub("'", "")
605 end
606
607 if username then
608 username = username:gsub("'", "")
609 end
610
611 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
612 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
613 return os.execute(cmd)
614 end
615
616
617 --- LuCI system utilities / wifi related functions.
618 -- @class module
619 -- @name luci.sys.wifi
620 wifi = {}
621
622 --- Get iwconfig output for all wireless devices.
623 -- @return Table of tables containing the iwconfing output for each wifi device
624 function wifi.getiwconfig()
625 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
626 local iwc = {}
627
628 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
629 local k = l:match("^(.-) ")
630 l = l:gsub("^(.-) +", "", 1)
631 if k then
632 local entry, flags = _parse_mixed_record(l)
633 if entry then
634 entry.flags = flags
635 end
636 iwc[k] = entry
637 end
638 end
639
640 return iwc
641 end
642
643 --- Get iwlist scan output from all wireless devices.
644 -- @return Table of tables contaiing all scan results
645 function wifi.iwscan(iface)
646 local siface = iface or ""
647 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
648 local iws = {}
649
650 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
651 local k = l:match("^(.-) ")
652 l = l:gsub("^[^\n]+", "", 1)
653 l = luci.util.trim(l)
654 if k then
655 iws[k] = {}
656 for j, c in pairs(luci.util.split(l, "\n Cell")) do
657 c = c:gsub("^(.-)- ", "", 1)
658 c = luci.util.split(c, "\n", 7)
659 c = table.concat(c, "\n", 1)
660 local entry, flags = _parse_mixed_record(c)
661 if entry then
662 entry.flags = flags
663 end
664 table.insert(iws[k], entry)
665 end
666 end
667 end
668
669 return iface and (iws[iface] or {}) or iws
670 end
671
672 --- Get available channels from given wireless iface.
673 -- @param iface Wireless interface (optional)
674 -- @return Table of available channels
675 function wifi.channels(iface)
676 local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
677 local cns = { }
678
679 local fd = io.popen(cmd)
680 if fd then
681 local ln, c, f
682 while true do
683 ln = fd:read("*l")
684 if not ln then break end
685 c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
686 if c and f then
687 cns[tonumber(c)] = tonumber(f)
688 end
689 end
690 fd:close()
691 end
692
693 if not next(cns) then
694 cns = {
695 2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
696 2.442, 2.447, 2.452, 2.457, 2.462
697 }
698 end
699
700 return cns
701 end
702
703
704 --- LuCI system utilities / init related functions.
705 -- @class module
706 -- @name luci.sys.init
707 init = {}
708 init.dir = "/etc/init.d/"
709
710 --- Get the names of all installed init scripts
711 -- @return Table containing the names of all inistalled init scripts
712 function init.names()
713 local names = { }
714 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
715 names[#names+1] = luci.fs.basename(name)
716 end
717 return names
718 end
719
720 --- Test whether the given init script is enabled
721 -- @param name Name of the init script
722 -- @return Boolean indicating whether init is enabled
723 function init.enabled(name)
724 if luci.fs.access(init.dir..name) then
725 return ( call(init.dir..name.." enabled") == 0 )
726 end
727 return false
728 end
729
730 --- Get the index of he given init script
731 -- @param name Name of the init script
732 -- @return Numeric index value
733 function init.index(name)
734 if luci.fs.access(init.dir..name) then
735 return call("source "..init.dir..name.."; exit $START")
736 end
737 end
738
739 --- Enable the given init script
740 -- @param name Name of the init script
741 -- @return Boolean indicating success
742 function init.enable(name)
743 if luci.fs.access(init.dir..name) then
744 return ( call(init.dir..name.." enable") == 1 )
745 end
746 end
747
748 --- Disable the given init script
749 -- @param name Name of the init script
750 -- @return Boolean indicating success
751 function init.disable(name)
752 if luci.fs.access(init.dir..name) then
753 return ( call(init.dir..name.." disable") == 0 )
754 end
755 end
756
757
758 -- Internal functions
759
760 function _parse_delimited_table(iter, delimiter, callback)
761 delimiter = delimiter or "%s+"
762
763 local data = {}
764 local trim = luci.util.trim
765 local split = luci.util.split
766
767 local keys = split(trim(iter()), delimiter, nil, true)
768 for i, j in pairs(keys) do
769 keys[i] = trim(keys[i])
770 end
771
772 for line in iter do
773 local row = {}
774 line = trim(line)
775 if #line > 0 then
776 for i, j in pairs(split(line, delimiter, nil, true)) do
777 if keys[i] then
778 row[keys[i]] = j
779 end
780 end
781 end
782
783 if callback then
784 callback(row)
785 else
786 data[#data+1] = row
787 end
788 end
789
790 return data
791 end
792
793 function _parse_mixed_record(cnt, delimiter)
794 delimiter = delimiter or " "
795 local data = {}
796 local flags = {}
797
798 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
799 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
800 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
801
802 if k then
803 if x == "" then
804 table.insert(flags, k)
805 else
806 data[k] = v
807 end
808 end
809 end
810 end
811
812 return data, flags
813 end