1. How to create an UUID (Version 1)

Here is my code to generate an universally unique identifier (UUID) version 1 that conforms to RFC4122. It needs OE4.1 on Linux, libpcap0.8 (to detect main network interface).

include std/math.e 
include std/datetime.e 
include std/os.e 
include std/dll.e 
include std/machine.e 
include std/socket.e 
 
-- UUID 
-- 
-- A UUID is 128 bit long, and consists of a 60-bit time value, a 16-bit sequence number and a 48-bit node identifier. 
-- 
-- <time>-<sequence>-<mac address> 
--   60       16           48 
-- <time> number of 100-nanosecond intervals since midnight 15 October 1582 Coordinated Universal Time (UTC), the date on which the Gregorian calendar was first adopted. 
-- 
-- In its canonical textual representation, the sixteen octets of a UUID are represented as 32 hexadecimal (base 16) digits, displayed in five groups separated by hyphens, in the form 8-4-4-4-12 for a total of 36 characters (32 alphanumeric characters and four hyphens). For example: 
-- 
--     123e4567-e89b-12d3-a456-426655440000 
--     xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx 
-- 
-- The four bits of digit M indicate the UUID version, and the one to three most significant bits of digit N indicate the UUID variant. In the example, M is 1 and N is a (10xx), meaning that the UUID is a variant 1, version 1 UUID; that is, a time-based DCE/RFC 4122 UUID. 
-- 
-- The canonical 8-4-4-4-12 format string is based on the "record layout" for the 16 bytes of the UUID:[2] 
-- 
-- UUID record layout 
-- 
-- Name                             Length      Length     Contents 
--                                  (bytes)  (hex digits) 
-- time_low                            4          8        integer giving the low 32 bits of the time 
-- time_mid                            2          4        integer giving the middle 16 bits of the time 
-- time_hi_and_version                 2          4        4-bit "version" in the most significant bits, followed by the high 12 bits of the time 
-- clock_seq_hi_and_res clock_seq_low  2          4        1-3 bit "variant" in the most significant bits, followed by the 13-15 bit clock sequence 
-- node                                6         12        the 48-bit node id 
 
------------------------------------------------------------------------------ 
 
public constant 
  NORMAL = 1, REVERSE = 2 
 
constant 
  PCAP_ERRBUF_SIZE = 256 
 
constant 
  libc = open_dll("") 
 
atom pcap = -1 
 
ifdef UNIX then 
  sequence un = uname() 
  sequence arch = un[5] 
  -- following lines work for Ubuntu derivatives 
  -- with other Linux distributions please update corresponding lines 
  if equal(arch, "i686") then 
    pcap = open_dll("/usr/lib/i386-linux-gnu/libpcap.so") 
  elsif equal(arch, "x86_64") then 
    pcap = open_dll("/usr/lib/x86_64-linux-gnu/libpcap.so") 
  elsif equal(arch, "armv7l") then 
    pcap = open_dll("/usr/lib/arm-linux-gnueabihf/libpcap.so.0.8") 
  end if 
end ifdef 
if pcap = -1 then 
  puts(1,"Could not open libpcap library!\n") 
  abort(0) 
end if 
 
constant 
  xpcap_lookupdev = define_c_func(pcap, "pcap_lookupdev", {C_POINTER}, C_POINTER), 
  ioctl_         = define_c_func(libc, "ioctl",{C_INT,C_INT,C_INT},C_INT), 
  socket_        = define_c_func(libc, "socket",{C_INT,C_INT,C_INT},C_INT) 
 
------------------------------------------------------------------------------ 
 
function hex_string(sequence s) 
  sequence result = "" 
  for i = 1 to length(s) do 
    result &= sprintf("%02x", s[i]) 
  end for 
  return result 
end function 
 
------------------------------------------------------------------------------- 
 
function uint_to_bytes(atom n, integer digits, integer direction=REVERSE) 
  sequence s = repeat(0, digits) 
  if direction = NORMAL then 
    for i = digits to 1 by -1 do 
      s[i] = remainder(n, #100) 
      n = floor(n / #100) 
    end for 
  elsif direction = REVERSE then 
    for i = 1 to digits do 
      s[i] = remainder(n, #100) 
      n = floor(n / #100) 
    end for 
  end if 
  return s 
end function 
 
------------------------------------------------------------------------------ 
 
function get_iface_hw_addr(sequence iface_name) 
  sequence hw_addr 
  atom ifreq, sockid, status 
 
  hw_addr = "" 
  ifreq = allocate(200) 
  sockid = c_func(socket_,{AF_INET,SOCK_DGRAM,0}) 
  if sockid < 0 then 
    free(ifreq) 
    return {} 
  end if 
  poke(ifreq,iface_name&0) 
  status = c_func(ioctl_,{sockid,#8927,ifreq}) -- HW Address 
  if status >= 0 then 
    hw_addr = "" 
    for ctr = 18 to 23 do 
      hw_addr = hw_addr & sprintf("%02x",peek(ifreq+ctr)) 
    end for 
    hw_addr = hw_addr[1..length(hw_addr)] 
  end if 
  return hw_addr 
end function 
 
------------------------------------------------------------------------------ 
 
function pcap_lookupdev() 
  atom error_buffer = allocate(PCAP_ERRBUF_SIZE) 
  sequence ret 
  atom device = c_func(xpcap_lookupdev, {error_buffer}) 
  if device = NULL then 
    ret = {0, peek_string(error_buffer)} 
  end if 
  free(error_buffer) 
  ret = {1, peek_string(device)} 
  return ret 
end function 
 
------------------------------------------------------------------------------ 
 
function gregorian_time() 
  sequence dt = now_gmt() 
  sequence from = new(1582,10,15, 0, 0, 0) 
  atom nb = trunc(diff(from, dt)*10_000_000) 
  return nb 
end function 
 
------------------------------------------------------------------------------ 
 
function create_uuid() 
  sequence hw_addr, iface_name 
  integer ok 
 
  sequence uuid = "" 
  sequence s = uint_to_bytes(gregorian_time(), 8, NORMAL) 
  sequence gt = hex_string(s) 
  sequence time_low = tail(gt, 8) 
  gt = gt[1..$-8] 
  sequence time_mid = tail(gt, 4) 
  gt = gt[1..$-4] 
  sequence time_hi = tail(gt, 3) 
 
  sequence dt = now() 
  sequence midnight = dt[1..3] & {0,0,0} 
  s = uint_to_bytes(remainder(diff(midnight, dt),3600), 2, NORMAL) 
  sequence clock_seq = hex_string(s) 
  integer n = clock_seq[1] - #30 
  clock_seq[1] = #30 + (8 + and_bits(n, #3)) 
 
  {ok, iface_name} = pcap_lookupdev() 
  if ok then 
    hw_addr = get_iface_hw_addr(iface_name) 
  else 
    puts(2, "No interface found!\n") 
  end if 
 
  uuid = time_low & "-" & time_mid & "-1" &  time_hi & 
         "-" & clock_seq & "-" & hw_addr 
  return uuid 
end function 
 
------------------------------------------------------------------------------ 
 
sequence s = create_uuid() 
puts(1, s & "\n") 

libpcap cant be forgotten if you specify the default interface as a parameter to create_uuid().

Regards

Jean-Marc

new topic     » topic index » view message » categorize

2. Re: How to create an UUID (Version 1)

Here is the short version without libpcap and UUID explanations:

include std/math.e 
include std/datetime.e 
include std/os.e 
include std/dll.e 
include std/machine.e 
include std/socket.e 
 
public constant 
  NORMAL = 1, REVERSE = 2, 
  SIOCGIFHWADDR = #8927 
 
constant 
  libc = open_dll("") 
 
constant 
  ioctl_         = define_c_func(libc, "ioctl",{C_INT,C_INT,C_INT},C_INT), 
  socket_        = define_c_func(libc, "socket",{C_INT,C_INT,C_INT},C_INT) 
 
------------------------------------------------------------------------------ 
 
function hex_string(sequence s) 
  sequence result = "" 
  for i = 1 to length(s) do 
    result &= sprintf("%02x", s[i]) 
  end for 
  return result 
end function 
 
------------------------------------------------------------------------------- 
 
function uint_to_bytes(atom n, integer digits, integer direction=REVERSE) 
  sequence s = repeat(0, digits) 
  if direction = NORMAL then 
    for i = digits to 1 by -1 do 
      s[i] = remainder(n, #100) 
      n = floor(n / #100) 
    end for 
  elsif direction = REVERSE then 
    for i = 1 to digits do 
      s[i] = remainder(n, #100) 
      n = floor(n / #100) 
    end for 
  end if 
  return s 
end function 
 
------------------------------------------------------------------------------ 
 
function get_iface_hw_addr(sequence iface_name) 
  sequence hw_addr 
  atom ifreq, sockid, status 
 
  hw_addr = "" 
  ifreq = allocate(200) 
  sockid = c_func(socket_,{AF_INET,SOCK_DGRAM,0}) 
  if sockid < 0 then 
    free(ifreq) 
    return {} 
  end if 
  poke(ifreq,iface_name&0) 
  status = c_func(ioctl_,{sockid, SIOCGIFHWADDR, ifreq}) -- HW Address 
  if status >= 0 then 
    hw_addr = "" 
    for ctr = 18 to 23 do 
      hw_addr = hw_addr & sprintf("%02x", peek(ifreq+ctr)) 
    end for 
    hw_addr = hw_addr[1..length(hw_addr)] 
  end if 
  return hw_addr 
end function 
 
------------------------------------------------------------------------------ 
 
function gregorian_time() 
  sequence dt = now_gmt() 
  sequence from = new(1582,10,15, 0, 0, 0) 
  atom nb = trunc(diff(from, dt)*10_000_000) 
  return nb 
end function 
 
------------------------------------------------------------------------------ 
 
function create_uuid(sequence iface_name) 
  sequence hw_addr 
  integer ok 
 
  sequence uuid = "" 
  sequence s = uint_to_bytes(gregorian_time(), 8, NORMAL) 
  sequence gt = hex_string(s) 
  sequence time_low = tail(gt, 8) 
  gt = gt[1..$-8] 
  sequence time_mid = tail(gt, 4) 
  gt = gt[1..$-4] 
  sequence time_hi = tail(gt, 3) 
 
  sequence dt = now() 
  sequence midnight = dt[1..3] & {0,0,0} 
  s = uint_to_bytes(remainder(diff(midnight, dt),3600), 2, NORMAL) 
  sequence clock_seq = hex_string(s) 
  integer n = clock_seq[1] - #30 
  clock_seq[1] = #30 + (8 + and_bits(n, #3)) 
 
  hw_addr = get_iface_hw_addr(iface_name) 
 
  uuid = time_low & "-" & time_mid & "-1" &  time_hi & 
         "-" & clock_seq & "-" & hw_addr 
  return uuid 
end function 
 
------------------------------------------------------------------------------ 
 
sequence s = create_uuid("eth0") 
puts(1, s & "\n") 
new topic     » goto parent     » topic index » view message » categorize

3. Re: How to create an UUID (Version 1)

Even simpler:

include std/rand.e 
-- include lib/_rand_.e  -- better randomization using EU4 Standard library 
include std/console.e 
 
function gen_uuid() 
-- better randomization 
  -- initialize() 
  -- randomize() 
-- or standard one 
  set_rand(time()) 
 
  -- following line converted from PHP: https://www.ts-services.com/generation-uuid-php/ 
  return sprintf( 
    "%04x%04x-%04x-%04x-%04x-%04x%04x%04x", 
    { 
      rand_range(0, #FFFF), 
      rand_range(0, #FFFF), 
      rand_range(0, #FFFF), 
      or_bits(rand_range(0, #0FFF), #4000), 
      or_bits(rand_range(0, #3FFF), #8000), 
      rand_range(0, #FFFF), 
      rand_range(0, #FFFF), 
      rand_range(0, #FFFF) 
    } 
 ) 
end function 
 
puts(1, gen_uuid() & "\n") 
maybe_any_key() 

Jean-Marc

new topic     » goto parent     » topic index » view message » categorize

4. Re: How to create an UUID (Version 1)

You know what? This kinda thing should probably just be in the OpenEuphoria standard library. And now it will be!

https://github.com/OpenEuphoria/euphoria/pull/4

I'll merge these changes soon unless there are any objections.

-Greg

new topic     » goto parent     » topic index » view message » categorize

5. Re: How to create an UUID (Version 1)

ghaberek said...

unless there are any objections.

I am not an expert on these things but off the top of my head:

Windows: fine

Linux: datetime+mac = fine, purely random = not really, needs an option

Personally, I think that "new()" sucks, what is wrong with "new_guid()"??
It should, imo, be new_guid(bool bAllowRand=false) and return (say) {} when uuid_generate=-1 and bAllowRand is false.

Pete

new topic     » goto parent     » topic index » view message » categorize

6. Re: How to create an UUID (Version 1)

Because of the way Eu handles namespaces, I too would prefer that no more "new" keywords were added. Seems better (and causes less namespace clashes) if functions get a more descriptive name.

new topic     » goto parent     » topic index » view message » categorize

7. Re: How to create an UUID (Version 1)

petelomax said...

Linux: datetime+mac = fine, purely random = not really, needs an option

Yeah the problem is that libuuid doesn't seem to be installed by default on common distros, at least not in my initial testing on Ubuntu and Fedora.

petelomax said...

It should, imo, be new_guid(bool bAllowRand=false) and return (say) {} when uuid_generate=-1 and bAllowRand is false.

According to the documentation, uuid_generate is going to create a random UUID by default:

The uuid_generate function creates a new universally unique identifier (UUID). The uuid will be generated
based on high-quality randomness from /dev/urandom, if available. If it is not available, then
uuid_generate will use an alternative algorithm which uses the current time, the local ethernet MAC
address (if available), and random data generated using a pseudo-random generator.

In that case, I could follow the same method: read bytes from /dev/urandom if I can, otherwise generate it based on the current time, MAC address, and rand().

petelomax said...

Personally, I think that "new()" sucks, what is wrong with "new_guid()"??

I'd prefer to keep it "UUID" since that is the term used by the system libraries. "GUID" is the Microsoft-specific term.

Do we want to just use the libuuid names? uuid_generate, uuid_parse, uuid_unparse (or maybe uuid_format?)

irv said...

Because of the way Eu handles namespaces, I too would prefer that no more "new" keywords were added. Seems better (and causes less namespace clashes) if functions get a more descriptive name.

I was unaware anyone felt this way. I will change it though. I'm indifferent and I can see what you mean.

Side topic: should we be making an effort to roll back the heavy namespace usage?

-Greg

new topic     » goto parent     » topic index » view message » categorize

8. Re: How to create an UUID (Version 1)

petelomax said...
ghaberek said...

unless there are any objections.

I am not an expert on these things but off the top of my head:

Windows: fine

Linux: datetime+mac = fine, purely random = not really, needs an option

Personally, I think that "new()" sucks, what is wrong with "new_guid()"??
It should, imo, be new_guid(bool bAllowRand=false) and return (say) {} when uuid_generate=-1 and bAllowRand is false.

Pete

First and second version conform to RFC4122 and do not make use of randomization, so uniqueness is guaranteed as far as possible.

Jean-Marc

new topic     » goto parent     » topic index » view message » categorize

9. Re: How to create an UUID (Version 1)

ghaberek said...

According to the documentation, uuid_generate is going to create a random UUID by default:

The uuid_generate function creates a new universally unique identifier (UUID). The uuid will be generated
based on high-quality randomness from /dev/urandom, if available. If it is not available, then
uuid_generate will use an alternative algorithm which uses the current time, the local ethernet MAC
address (if available), and random data generated using a pseudo-random generator.

In that case, I could follow the same method: read bytes from /dev/urandom if I can, otherwise generate it based on the current time, MAC address, and rand().

I can't remember where I read that yesterday but even libuuid is not guaranteed to provide unique identifiers and uuid_generate is based on libuuid.

Jean-Marc

new topic     » goto parent     » topic index » view message » categorize

10. Re: How to create an UUID (Version 1)

ghaberek said...

I'd prefer to keep it "UUID" since that is the term used by the system libraries. "GUID" is the Microsoft-specific term.

Do we want to just use the libuuid names? uuid_generate, uuid_parse, uuid_unparse (or maybe uuid_format?)

Sounds good to me, I suppose that instead of a 2-way bAllowRand it should probably likewise be a 3-way force rand/force mac/don't care(==default).

ghaberek said...
petelomax said...

Personally, I think that "new()" sucks, what is wrong with "new_guid()"??

irv said...

Because of the way Eu handles namespaces, I too would prefer that no more "new" keywords were added. Seems better (and causes less namespace clashes) if functions get a more descriptive name.

I was unaware anyone felt this way. I will change it though. I'm indifferent and I can see what you mean.

Actually, I can now explain my main objection: An index cannot hold 38 varieties of "new".
Sure, a tree-based index (ie one printed on paper) can have 38 page numbers after each entry, but each word in a chm file links to one and only one page.
After finally saying it like that, it has just become extra crystal clear.

ghaberek said...

Side topic: should we be making an effort to roll back the heavy namespace usage?

Just try not to make make it any worse, but there is no sense breaking your back.

Pete

new topic     » goto parent     » topic index » view message » categorize

11. Re: How to create an UUID (Version 1)

So I've been doing a lot of reading about UUIDs.

Obviously there are a lot of opinions on the topic. I think the best synopsis this comes from this article: AppSec Concerns: UUID Generation.

  • Generate UUIDs on the server, if possible. This will ensure a sufficiently strong algorithm can be used to generate the UUID.
  • Use a strong UUID algorithm. Choosing a cryptographically secure PRNG will reduce the chance of a collision. Most major development platforms have secure UUID libraries (e.g., Java’s uuid.randomUUID() method).
  • Plan for collisions. Ensure that applications and servers behave safely and consistently if they encounter identical UUIDs for different records or objects.
  • Don’t rely on UUIDs for security. Never use UUIDs for things like session identifiers. The standard itself warns implementors to “not assume that UUIDs are hard to guess; they should not be used as security capabilities (identifiers whose mere possession grants access, for example).”

I'll add to that:

  • UUIDs which include a MAC address provide sensitive information about your system. By using uuid_generate/UuidCreate or random numbers, this will not be included.
  • UUIDs which are time-based will always be chronological. Again, this is sensitive information about the system, which could be used by an attacker, and should be avoided.

Based on all that, here's my suggested implementation:

public enum type uuid_source_t 
 
    -- attempt SYSTEM, then RANDOM, then LEGACY 
    UUID_SOURCE_DEFAULT = 0, 
 
    -- use the system library to generate a UUID. 
    -- return {} if no system library exists 
    UUID_SOURCE_SYSTEM, 
 
    -- use the system CSPRNG to generate a UUID. 
    -- return {} if no CSPRNG source exists 
    UUID_SOURCE_RANDOM, 
 
    -- generate a timestamp + random MAC address UUID. 
    -- always returns a value 
    UUID_SOURCE_LEGACY 
 
end type 
 
-- 
-- Returns TRUE if we have a system library for generating UUIDs. 
-- 
public function uuid_have_system_source() 
 
-- 
-- Returns TRUE if we have a system-wide CSPRNG source for UUIDs. 
-- 
public function uuid_have_random_source() 
 
-- 
-- Returns which method to be used by UUID_SOURCE_DEFAULT. 
-- 
public function uuid_default_source() 
 
-- 
-- Generate a new UUID. 
-- 
public function uuid_generate( uuid_source_t source = UUID_SOURCE_DEFAULT ) 
 
-- 
-- Convert a UUID to its string representation. 
-- 
public function uuid_format( uuid_t uuid ) 
 
-- 
-- Parse a string into a UUID. 
-- 
public function uuid_parse( string str ) 

-Greg

new topic     » goto parent     » topic index » view message » categorize

12. Re: How to create an UUID (Version 1)

ghaberek said...

Side topic: should we be making an effort to roll back the heavy namespace usage?

I like how Euphoria does namespacing. I say keep it. And keep new() in the UUID lib.

new topic     » goto parent     » topic index » view message » categorize

13. Re: How to create an UUID (Version 1)

I'm also indifferent but I did want to nitpick one minor detail...

petelomax said...

Actually, I can now explain my main objection: An index cannot hold 38 varieties of "new".
Sure, a tree-based index (ie one printed on paper) can have 38 page numbers after each entry, but each word in a chm file links to one and only one page.
After finally saying it like that, it has just become extra crystal clear.

Why not add some new eudoc so the chm indexer will create entries called "new (uuid)" and also "new (eumem)" and maybe "new (socketio)" or some variant thereof? Each of these will be unique so we'll have 38 "new (..blah..)" links.

Why should we warp the language to compenstate for some deficiency in some other tool?

new topic     » goto parent     » topic index » view message » categorize

14. Re: How to create an UUID (Version 1)

-- more legible 
 
   new()     -- invitation for nameclashes 
re_new()     -- traditional Euphoria 
re.new()     -- dot notation style 
re:new()     -- namespace 
 reNew()     -- camelCase 
 
-- less legible 

The underscore _ gives you "more whitespace" while "connecting" at the same time. Number of characters does not suffer.

One should not mention .chm in polite company.

The OE docs should probably be revised to show namespaced identifiers all the time.

  • since you must know which include is needed
  • will make searching for a particular "new" easier

With Phix autoinclude this stuff is a bit easier.

_tom

new topic     » goto parent     » topic index » view message » categorize

15. Re: How to create an UUID (Version 1)

One case for a nasty bug waiting to happen (in Phix):

--main.exw 
include somelib.e 
include inc1.e 
--inc1.e 
--include someotherlib.e 
include inc2.e 
--inc2.e 
x = new() 

Without including someotherlib.e that last line is equivalent to somelib:new().
Uncommenting/adding someotherlib.e quietly changes it to someotherlib:new().

So you can break code in some remote location without apparently editing any of the files involved.

Of course things are fine if you always remember to use the correct namespace, re-include at the right level, and/or if there are enough namespace clashes to trigger compilation errors.

Pete

new topic     » goto parent     » topic index » view message » categorize

16. Re: How to create an UUID (Version 1)

The namespace usage gives us the ability to make drop in replacements of one library for another. Now, you really only benefit from a better implementation this way. Suppose someone writes an improved regex library and all of your code is using std/regex.e as re. You simply replace the include lines with "include my_betterrelibrary.e as re" and you are good to go.

So, that's why we use namespaces. As for always using new for creating new things. As a developer of a Euphoria do you want to use a thesaurus in order to name the constructor? As a user I don't want to remember whether the developer called it new, or create, or some other thing. Let's try to keep things consistent.

I open the HTML docs and type in new and I see all the options. A similar thing happens when using neon-eclipse as between GUI frameworks there copies of terms like Iterator that exist in both but are different things so the software offers to include anyone that has iterator. An editor has a lot of context, and also a browser does. As a proposed change if you happen to be on the regex page before searching, the javascript could highlight regex's new in bold, for example.

Let's keep 'new' short and sweet.

SD Pringle

new topic     » goto parent     » topic index » view message » categorize

17. Re: How to create an UUID (Version 1)

SDPringle said...

The namespace usage gives us the ability to make drop in replacements of one library for another. Now, you really only benefit from a better implementation this way. Suppose someone writes an improved regex library and all of your code is using std/regex.e as re. You simply replace the include lines with "include my_betterrelibrary.e as re" and you are good to go.

'nuff said. grin

new topic     » goto parent     » topic index » view message » categorize

18. Re: How to create an UUID (Version 1)

Sorry, been afk a few days...

euphoric said...
SDPringle said...

The namespace usage gives us the ability to make drop in replacements of one library for another.

'nuff said. grin

You both know that is a pathetic excuse for meaningless names.

SDPringle said...

I open the HTML docs and type in new and I see all the options.

Oh really? https://openeuphoria.org/search/results.wc?manual=1&s=new

new topic     » goto parent     » topic index » view message » categorize

19. Re: How to create an UUID (Version 1)

SDPringle said...

I open the HTML docs and type in new and I see all the options.

It's plausible. Opening and searching the HTML docs locally is different from using the builtin search engine on your website, which is powered by mysql and has some limitations, see

https://stackoverflow.com/questions/1585611/mysql-full-text-search-for-words-with-three-or-less-letters

https://stackoverflow.com/questions/13049306/fulltext-search-on-mysql-with-a-3-letter-word

new topic     » goto parent     » topic index » view message » categorize

20. Re: How to create an UUID (Version 1)

Still not exactly brilliant: https://openeuphoria.org/search/results.wc?s=new%2A&page=1&per_page=100&news=0&ticket=0&forum=0&wiki=0&manual=1

new topic     » goto parent     » topic index » view message » categorize

21. Re: How to create an UUID (Version 1)

So, here's a feature suggestion for when we rewrite the website: search dorks.

Basically, we can add support for search operators, so you if you search for something like section:docs name:new you'll only get entries for all new() routines.

We can also have an "advanced search" form or a separate search field on the docs page.

-Greg

new topic     » goto parent     » topic index » view message » categorize

Search



Quick Links

User menu

Not signed in.

Misc Menu