Introduction
On my Windows 7 machine, I happened upon the Time Limits dialog used by Parental Controls.
This intrigued me enough to wonder how this information might be obtained. I quickly found the NetUserGetInfo() function. This function populates one of nine different USER_INFO_x
structures with various information about a particular user account on a server. However, each time I found an example of what the members of the structure looked like, they all treated the usriX_logon_hours
member the same: as 21 separate bytes. Printing the other structure members formatted with %s
or %d
is fine, but I found nothing useful from looking at that member formatted with %x
. Thus, my mission began...
Reading the Array
MSDN tells us that the usriX_logon_hours
member is a 168-bit array (laid out from 0 to 167) with each bit representing an hour of the day. I set one of the user accounts on my computer to have the following allowed times: Sun 13-20; Mon-Thu, Sat 9-20; Fri 9-21. If you laid this array out so that it looked like a typical 24-hour week, you'd have the following (with the alternating shades of gray showing the byte boundaries).
| 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 | 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 |
Sun | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
Mon | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
Tue | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
Wed | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
Thu | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
Fri | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
Sat | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
So, how to read the 21-byte array so that it looks something like the above? Reading from the array is simple enough, but we're dealing with bits not bytes. I found it easier to look at the array as a 1x168 bit array rather than a 1x21 byte or a 7x24 bit array. The first order of business, then, will be to "convert" the 21 bytes into 168 bits. I did this with something that looks like:
LPBYTE lpLogonHours = lpUserInfo->usri2_logon_hours;
int nBits[7 * 24];
for (int x = 0; x < 21; x++)
{
for (int y = 7; y >= 0; y--)
nBits[z++] = (*lpLogonHours >> y) & 0x01;
lpLogonHours++;
}
This produces a 168-bit array, arranged in a familiar 7x24 table, that looks like:
| 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 | 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 |
Sun | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
Mon | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Tue | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Wed | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Thu | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Fri | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Sat | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Hmm, this does not look quite like what we are after. The bits appear to have some sense of uniformity, but some things don't quite line up. For example, I would have expected Sunday to contain seven consecutive 1s for the 13:00-20:00 hour slots.
MSDN tells us that the bits must be adjusted (i.e., shifted) according to our time zone. With UTC±0, for example, the first bit (of the first byte) is Sunday, 0:00 to 0:59; the second bit is Sunday, 1:00 to 1:59; and so on like above. Ok, but since I am in the UTC-6 timezone, the starting bit for me would be 6 (of the first byte). That would be my Sunday from 0:00 to 0:59; my Sunday from 1:00 to 1:59 would be bit 7; and so on. Also, since my Sunday started with bit 6, bits 0-5 are the last 6 hours of Saturday (this "wrapping" will become more apparent). The first 24 hours now look like:
Sat 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 | Sun 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 |
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
It changed, but does not really look any better. Let's see if there's anything else that needs tweaking.
Reading Each Byte in Reverse Order
Looking at the first table, I should see a 1 in the 18:00-19:00 slots for Saturday, but I'm seeing a 0 instead. I do, however, see two 1s at the other end of that byte. Also, I should see 1s in the 13:00-17:00 slots for Sunday (the last five hours), but I'm seeing them in the 10:00-14:00 slots instead (the first five hours). In both cases, it appears that the bits are backwards for each byte. When converting the bytes to bits, it seems maybe we should simply iterate the bits in the opposite order, like:
LPBYTE lpLogonHours = lpUserInfo->usri2_logon_hours;
int nBits[7 * 24];
for (int x = 0; x < 21; x++)
{
for (int y = 0; y < 8; y++)
nBits[z++] = (*lpLogonHours >> y) & 0x01;
lpLogonHours++;
}
The table now looks like the following. Notice how since the Sunday 0:00-0:59 hour has been offset by 6 hours from the beginning of the array, all subsequent hours have been offset as well, with the last 6 hours of Saturday being wrapped back around to the beginning of the array.
Sat 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 | Sun 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 |
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
Sun 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 | Mon 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 |
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Mon 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 | Tue 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 |
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Tue 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 | Wed 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 |
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Wed 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 | Thu 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 |
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Thu 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 | Fri 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 |
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Fri 18:00 | 19:00 | 20:00 | 21:00 | 22:00 | 23:00 | Sat 0:00 | 1:00 | 2:00 | 3:00 | 4:00 | 5:00 | 6:00 | 7:00 | 8:00 | 9:00 | 10:00 | 11:00 | 12:00 | 13:00 | 14:00 | 15:00 | 16:00 | 17:00 |
1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Much better!
Find the Right Starting Point in the Array
When we go to create the table at the top of this article, it's simply a matter of finding the right bit in the array to start reading from. By looking at each of the timezones and starting offsets in this table, we can see where we should start reading from, and we also might see a pattern:
If TZ is... | Then array offset is... |
-12 | 12 |
-11 | 11 |
-10 | 10 |
-9 | 9 |
-8 | 8 |
-7 | 7 |
-6 | 6 |
-5 | 5 |
-4 | 4 |
-3 | 3 |
-2 | 2 |
-1 | 1 |
0 | 0 |
1 | 167 |
2 | 166 |
3 | 165 |
4 | 164 |
5 | 163 |
6 | 162 |
7 | 161 |
8 | 160 |
9 | 159 |
10 | 158 |
11 | 157 |
12 | 156 |
13 | 155 |
The way of achieving the starting offset first requires us to know how many hours there are between Coordinated Universal Time (UTC) and local time.
TIME_ZONE_INFORMATION tzi;
GetTimeZoneInformation(&tzi);
int nOffset = tzi.Bias / -60;
Second, we then adjust that value forward or backward, like:
nOffset = (168 - nOffset) % 168;
Now that we know where to start reading the array from, we can simply iterate through all 168 bits. But what happens when we get to the end of the array but haven't read all 168 bits yet? Answer: Just wrap back around to 0. One way to do this is:
nOffset = nOffset + 1;
if (168 == nOffset)
nOffset = 0;
Or if you're into brevity, a one-line solution would be:
nOffset = (nOffset + 1) % 168;
Epilogue
While I used hard-coded values like 24 and 168 in the code snippets above, it was just to make the text easier to read. In the accompanying sample, however, I used the #define
macros found in the lmaccess.h
file.
It was not my intention with this article to make a replacement for the Time Limits dialog used by Vista and Windows 7, nor was I interested in creating a full-blown "restriction" application. Several of those already exist. I simply wanted to show how to read the information. I leave "write" capabilities to the interested reader.
Enjoy!