Issue #250: Convert all Windows registry methods to use Unicode API jv
authorPatrik Svestka <patrik.svestka@gmail.com>
Wed, 05 Dec 2018 13:03:24 +0100
branchjv
changeset 23609 5f68aa2dc5e8
parent 23547 c69c97cec351
child 23610 69df1eb85674
Issue #250: Convert all Windows registry methods to use Unicode API All methods are now unicode only Fixed reading of unicode name and values from registry via valueNamed: - Added a REG_QWORD registry type (64bit number) - Added regression tests for the changes at RegressionTests::Win32OperatingSystemTest - removed all ASCII only reading -> only unicode version is now supported - replaced while(*cp++) itteration with wide-string search (wcschr) - faster & clear intent - added a character limit read to count the string lenght (REG_MULTI_SZ) - clearing the pointer after usage - renamed a method parameter aValueName to just name - added a nameUtf16Z which formats the input name into Unicode16String with terminating NULL - Added a comment on registry size limitations at #valueNamed:put: The #valueNameAtIndex: - The method is used at (all these now support unicode): - #valueNames - replaced nextPut: with nextPutUnicode: - #valueNamesAndValuesDo: - #valueNamesDo: The #subKeyAtIndex: - The method is used at (all these now support unicode): - #subKeysDo: - #allSubKeysDo: Others fixed: - #subKeyNameAndClassAtIndex: - #remoteKeyOnHost: - - added a hostNameUtf16Z which formats the input name into Unicode16String with terminating NULL - checking if registry value name is a string if not return nil and print message into transcript - defaultValue can be now used - reading a (default) value from a reagistry Key
Win32OperatingSystem.st
--- a/Win32OperatingSystem.st	Tue Nov 13 15:58:15 2018 +0100
+++ b/Win32OperatingSystem.st	Wed Dec 05 13:03:24 2018 +0100
@@ -16073,17 +16073,42 @@
 
 remoteKeyOnHost:hostName
     "return the corresponding registry entry from
-     a remote computers registry."
-
-    |newEntry remoteHandle errorNumber|
-
+     a remote computers registry.
+     Note: The registry key must be form a predefined list defined by Microsoft."
+
+    |hostNameUtf16Z newEntry remoteHandle errorNumber|
+
+    hostNameUtf16Z := hostName notEmptyOrNil ifTrue:[hostName asUnicode16StringZ].
+    
 %{
     HKEY myKey, remoteKey = 0;
     int _retVal;
 
-    if (__isExternalAddressLike(__INST(handle)) && __isStringLike(hostName)) {
+/* Notes from MSDN:
+ * link: https://docs.microsoft.com/en-us/windows/desktop/api/winreg/nf-winreg-regconnectregistryw
+ * 
+ * 1) RegConnectRegistry requires the Remote Registry service to be running on the remote computer. 
+ *    By default, this service is configured to be started manually. To configure the Remote Registry 
+ *    service to start automatically, run Services.msc and change the Startup Type of the service to Automatic.
+ * 
+ * 2) If the computer is joined to a workgroup and the "Force network logons using local accounts to authenticate as 
+ *    Guest" policy is enabled, the function fails. Note that this policy is enabled by default if the computer is joined 
+ *    to a workgroup.
+ * 
+ * 3) If the current user does not have proper access to the remote computer, the call to RegConnectRegistry fails. To connect
+ *    to a remote registry, call LogonUser with LOGON32_LOGON_NEW_CREDENTIALS and ImpersonateLoggedOnUser before calling 
+ *    RegConnectRegistry.
+ * 
+ * 4) myKey must be a predefined registry handle. 
+ *    more at: https://docs.microsoft.com/en-us/windows/desktop/SysInfo/predefined-keys
+ * 
+ * 5) When a handle returned by RegConnectRegistry is no longer needed, it should be closed by calling RegCloseKey. (done by registerForFinaliation)
+ *
+ */    
+
+    if (__isExternalAddressLike(__INST(handle)) && __isUnicode16String(hostNameUtf16Z)) {
 	myKey = (HKEY)__externalAddressVal(__INST(handle));
-	if ((_retVal = RegConnectRegistryA(__stringVal(hostName), myKey, &remoteKey)) == ERROR_SUCCESS) {
+	if ((_retVal = RegConnectRegistryW(__unicode16StringVal(hostNameUtf16Z), myKey, &remoteKey)) == ERROR_SUCCESS) {
 	    remoteHandle = __MKEXTERNALADDRESS(remoteKey);
 	} else {
 	    if ((_retVal != ERROR_PATH_NOT_FOUND)
@@ -16100,7 +16125,8 @@
     ].
     errorNumber notNil ifTrue:[
 	(OperatingSystem errorHolderForNumber:errorNumber) reportError.
-    ].
+    ].  
+
     ^ nil
 
     "
@@ -16131,31 +16157,46 @@
 
 subKeyAtIndex:subKeyIndex
     "return a new registry entry below mySelf for the given subKey index.
-     Return nil if no such key exists"
+     Return nil if no such key exists
+     WARNING: subKeyIndex is 0-based!!"
+
 
     |subKeyName subKeyClassName errorNumber|
 
 %{
     HKEY myKey, subKey = 0;
-    char nameBuffer[256];
-    DWORD nameSize = sizeof(nameBuffer) - 1;
-    char classNameBuffer[256];
-    DWORD classNameSize = sizeof(classNameBuffer) - 1;
+
+/* lpName (nameBuffer):
+ * A pointer to a buffer that receives the name of the subkey, including the 
+ * terminating null character. The function copies only the name of the subkey, 
+ * not the full key hierarchy, to the buffer. If the function fails, no information is copied to this buffer.
+ * 
+ * note: This actually means that the if the path fits within 255 chacaters you could get another 255 characters for the key itself.
+ *       This could help if you are having issues with the registry path lenght.
+ */
+    wchar_t nameBuffer[256];  // 256 is due to Key name limit (including path)
+    DWORD nameSize = sizeof(nameBuffer);
+ 
+ /* lpClass (classNameBuffer):
+  * A pointer to a buffer that receives the user-defined class of the enumerated subkey. 
+  */
+    wchar_t classNameBuffer[256];
+    DWORD classNameSize = sizeof(classNameBuffer);
     FILETIME modificationTime;
     int _retVal;
 
     if (__isExternalAddressLike(__INST(handle))
      && __isSmallInteger(subKeyIndex)) {
 	myKey = (HKEY)__externalAddressVal(__INST(handle));
-	if ((_retVal = RegEnumKeyExA(myKey, __intVal(subKeyIndex),
+	if ((_retVal = RegEnumKeyExW(myKey, __intVal(subKeyIndex),
 			 nameBuffer, &nameSize,
 			 NULL,
 			 classNameBuffer, &classNameSize,
 			 &modificationTime)) == ERROR_SUCCESS) {
 	    nameBuffer[nameSize] = '\0';
 	    classNameBuffer[classNameSize] = '\0';
-	    subKeyName = __MKSTRING(nameBuffer);
-	    subKeyClassName = __MKSTRING(classNameBuffer);
+	    subKeyName = __MKU16STRING(nameBuffer);
+	    subKeyClassName = __MKU16STRING(classNameBuffer);
 	} else {
 	    if ((_retVal != ERROR_PATH_NOT_FOUND)
 	     && (_retVal != ERROR_FILE_NOT_FOUND)
@@ -16183,31 +16224,46 @@
 
 subKeyNameAndClassAtIndex:subKeyIndex
     "return the name and className of the given subKey at index as a pair.
-     Return nil if no such key exists"
+     Return nil if no such key exists
+     WARNING: subKeyIndex is 0-based!!"
 
     |subKeyName subKeyClassName errorNumber|
 
 %{
     HKEY myKey, subKey = 0;
-    char nameBuffer[256];
-    DWORD nameSize = sizeof(nameBuffer) - 1;
-    char classNameBuffer[256];
-    DWORD classNameSize = sizeof(classNameBuffer) - 1;
+
+/* lpName (nameBuffer):
+ * A pointer to a buffer that receives the name of the subkey, including the 
+ * terminating null character. The function copies only the name of the subkey, 
+ * not the full key hierarchy, to the buffer. If the function fails, no information is copied to this buffer.
+ * 
+ * note: This actually means that the if the path fits within 255 chacaters you could get another 255 characters for the key itself.
+ *       This could help if you are having issues with the registry path lenght.
+ */
+    wchar_t nameBuffer[256];  // limiting the nameBuffer to 255 characters + null terminator
+    DWORD nameSize = sizeof(nameBuffer);
+
+ /* lpClass (classNameBuffer):
+  * A pointer to a buffer that receives the user-defined class of the enumerated subkey. 
+  */
+    wchar_t classNameBuffer[256]; // limiting the classNameBuffer to 255 characters + nul terminator
+    DWORD classNameSize = sizeof(classNameBuffer);
+
     FILETIME modificationTime;
     int _retVal;
 
     if (__isExternalAddressLike(__INST(handle))
      && __isSmallInteger(subKeyIndex)) {
 	myKey = (HKEY)__externalAddressVal(__INST(handle));
-	if ((_retVal = RegEnumKeyExA(myKey, __intVal(subKeyIndex),
+	if ((_retVal = RegEnumKeyExW(myKey, __intVal(subKeyIndex),
 			 nameBuffer, &nameSize,
 			 NULL,
 			 classNameBuffer, &classNameSize,
 			 &modificationTime)) == ERROR_SUCCESS) {
 	    nameBuffer[nameSize] = '\0';
-	    classNameBuffer[classNameSize] = '\0';
-	    subKeyName = __MKSTRING(nameBuffer);
-	    subKeyClassName = __MKSTRING(classNameBuffer);
+	    classNameBuffer[classNameSize] = '\0';        
+        subKeyName = __MKU16STRING(nameBuffer);
+        subKeyClassName = __MKU16STRING(classNameBuffer);
 	} else {
 	    if ((_retVal != ERROR_PATH_NOT_FOUND)
 	     && (_retVal != ERROR_FILE_NOT_FOUND)
@@ -16432,27 +16488,43 @@
 
 valueNameAtIndex:valueIndex
     "return a values name for the given value index.
-     Return nil if no such value exists"
+     Return nil if no such value exists
+     WARNING: valueIndex is 0-based!!"
 
     |valueName errorNumber|
 
 %{
     HKEY myKey;
-    char nameBuffer[256];
-    DWORD nameSize = sizeof(nameBuffer) - 1;
+
+/* nameBuffer (lpValueName in RegEnumValue function):
+ * A pointer to a buffer that receives the name of the value as a null-terminated string.
+ * This buffer must be large enough to include the terminating null character.
+ * For more information, see Registry Element Size Limits.
+ */
+  wchar_t nameBuffer[256]; // 256 is due to Key name limit (including path) + the null character
+
+/* nameSize (lpcchValueName in RegEnumValue function):
+ *
+ * A pointer to a variable that specifies the size of the buffer pointed to by the lpValueName parameter, in characters. 
+ * When the function returns, the variable receives the number of characters stored in the buffer, not including the terminating null character.
+ *
+ * Registry value names are limited to 32,767 bytes. The ANSI version of this function treats this parameter as a SHORT value. 
+ * Therefore, if you specify a value greater than 32,767 bytes, there is an overflow and the function may return ERROR_MORE_DATA.
+ */
+    DWORD nameSize = sizeof(nameBuffer);
     DWORD valueType;
     int _retVal;
 
     if (__isExternalAddressLike(__INST(handle))
      && __isSmallInteger(valueIndex)) {
 	myKey = (HKEY)__externalAddressVal(__INST(handle));
-	if ((_retVal = RegEnumValueA(myKey, __intVal(valueIndex),
+	if ((_retVal = RegEnumValueW(myKey, __intVal(valueIndex),
 			 nameBuffer, &nameSize,
 			 NULL,
 			 &valueType,
 			 NULL, NULL)) == ERROR_SUCCESS) {
 	    nameBuffer[nameSize] = '\0';
-	    valueName = __MKSTRING(nameBuffer);
+        valueName = __MKU16STRING(nameBuffer);
 	} else {
 	    if ((_retVal != ERROR_PATH_NOT_FOUND)
 	     && (_retVal != ERROR_FILE_NOT_FOUND)
@@ -16475,7 +16547,7 @@
     "
 !
 
-valueNamed:aValueName
+valueNamed:name
     "retrieve a value; the returned object depends upon the type:
 	REG_BINARY      -> ByteArray
 	REG_SZ          -> String
@@ -16484,59 +16556,108 @@
 	REG_NONE        -> nil
     "
 
-    |stringArray retVal errorNumber|
+    |nameUtf16Z stringArray retVal errorNumber|
+
+    "/ name must be a string
+    name isString ifFalse: [ 
+        Transcript showCR: 'The registry value name must be a String!!'.
+        ^ nil
+    ].
+
+    "/ adding terminating null into empty string
+    name notNil ifTrue:[
+        name isEmpty ifTrue:[nameUtf16Z := (name, (Character codePoint: 0)) asUnicode16String] "/ needed for defaultValue
+                    ifFalse:[nameUtf16Z := name asUnicode16StringZ]
+    ].
 
 %{  /* STACK: 20000 */
     HKEY myKey;
     DWORD valueType;
+    int val;  
     union {
-	DWORD dWord;
-	unsigned char dWordBytes[4];
-	unsigned char smallDataBuffer[1024*16];
+        DWORD dWord;
+        unsigned char dWordBytes[4];   // needed for the shifts at REG_DWORD_LITTLE_ENDIAN and REG_DWORD_BIG_ENDIAN
+        ULONGLONG qWord;               // needed for the 64-bit int (QWORD)
+        wchar_t wstringBuffer[1024*8]; // buffer for wide characters
     } quickData;
-    int val;
+
+    wchar_t *dataBuffer = NULL;
     DWORD dataSize = sizeof(quickData);
-    unsigned char *dataBuffer = NULL;
-#define xxUSE_UNICODE
-#ifdef USE_UNICODE
-# define xRegQueryValueEx  RegQueryValueExW
-# define CHAR             short
-#else
-# define RegQueryValueEx  RegQueryValueExA
-# define CHAR             char
-#endif
 
     if (__isExternalAddressLike(__INST(handle))
-     && __isStringLike(aValueName)) {
+     && __isUnicode16String(nameUtf16Z)) {
 	int ret;
 
 	myKey = (HKEY)__externalAddressVal(__INST(handle));
 
-	/*
-	 * try to get it with one call ...
-	 */
-	ret = RegQueryValueExA(myKey, __stringVal(aValueName),
+  /* Reading values from registry 
+   * 
+   * LSTATUS RegQueryValueEx(  // ends with either A(ascii) or W (unicode)
+   *   HKEY    hKey,         // a handle of a registry key
+   *   LPCWSTR lpValueName,  // The name of the registry value
+   *   LPDWORD lpReserved,   // reserved and must be NULL
+   *   LPDWORD lpType,       // A pointer to a variable that recieves a code indicating the type of data stored in the specified value
+   *   LPBYTE  lpData,       // A pointer to a buffer that receives the value's data.  This parameter can be NULL if datat is not required
+   *   LPDWORD lpcbData      // A pointer to a variable that specifies the size of the buffer pointed to by the IpData parameter, in bytes. 
+   *                          // When the furnction returns, this variable contains the size  of the data copied to IpData
+   * );
+  */
+
+#if 0
+   console_printf("================================================\n");
+   console_printf("Before - myKey: %p\n", &myKey);
+   console_printf("Before - aValueName: %S\n", __unicode16StringVal(nameUtf16Z));
+   console_printf("Before - valueType: %p, valueTypeData: %lu\n", &valueType, valueType);
+   console_printf("Before - QuickData dWord: %lu\n", quickData.dWord);
+   console_printf("Before - QuickData dWordBytes: %S\n", quickData.dWordBytes);
+   console_printf("Before - QuickData qWord: %lu\n", quickData.qWord);
+   console_printf("Before - QuickData smallDataBuffer: %S\n", quickData.smallDataBuffer);
+   console_printf("Before - Databuffer: %p, dataBufferData: %s\n", &dataBuffer, dataBuffer);
+   console_printf("Before - dataSize: %p, dataSizeData: %lu\n", &dataSize, dataSize);
+   console_printf("================================================\n");
+#endif
+    
+    /*
+     * try to get it with one call ...
+     */
+    ret = RegQueryValueExW(myKey, __unicode16StringVal(nameUtf16Z),
 			 NULL,
 			 &valueType,
-			 (char *)&quickData,
+             (LPBYTE) &quickData.wstringBuffer, // LPBYTE aka (unsigned char *)
 			 &dataSize);
+
+#if 0   
+   console_printf("------------------------------------------------\n");
+   console_printf("After - myKey: %p\n", &myKey);
+   console_printf("After - aValueName: %S\n", __unicode16StringVal(nameUtf16Z));
+   console_printf("After - valueType: %p, valueTypeData: %lu\n", &valueType, valueType);
+   console_printf("After - QuickData dWord: %lu\n", quickData.dWord);
+   console_printf("After - QuickData dWordBytes: %S\n", quickData.dWordBytes);
+   console_printf("After - QuickData qWord: %lu\n", quickData.qWord);
+   console_printf("After - QuickData smallDataBuffer: %S\n", quickData.smallDataBuffer);
+   console_printf("After - Databuffer: %p, dataBufferData: %s\n", &dataBuffer, dataBuffer);
+   console_printf("After - dataSize: %p, dataSizeData: %lu\n", &dataSize, dataSize);
+   console_printf("------------------------------------------------\n");
+#endif   
+
 #if 0
-	console_printf("get \"%s\": dataSize=%d ret=%d\n", __stringVal(aValueName), dataSize, ret);
+    console_printf("Registry key: %p\n", (void *) myKey);
+	console_printf("get \"%S\": dataSize=%d ret=%d\n", __unicode16StringVal(nameUtf16Z), dataSize, ret);
 #endif
 	while (ret == ERROR_MORE_DATA) {
 #if 0
-	    console_printf("ERROR_MORE_DATA dataSize=%d valueType=%d\n", dataSize, valueType);
+	console_printf("ERROR_MORE_DATA dataSize=%d valueType=%d\n", dataSize, valueType);
 #endif
 	    /*
 	     * nope - need another one ...
 	     */
-	    if (myKey = HKEY_PERFORMANCE_DATA) {
+	    if (myKey == HKEY_PERFORMANCE_DATA) {
 		dataSize = dataSize * 2;
 	    }
 	    switch (valueType) {
 		case REG_BINARY:
 		case REG_MULTI_SZ:
-		    dataBuffer = malloc(dataSize);;
+		    dataBuffer = malloc(dataSize);
 		    break;
 		case REG_SZ:
 		    dataBuffer = malloc(dataSize);
@@ -16546,10 +16667,10 @@
 		    break;
 	    }
 	    if (dataBuffer) {
-		ret = RegQueryValueEx(myKey, __stringVal(aValueName),
+		ret = RegQueryValueExW(myKey, __unicode16StringVal(nameUtf16Z),
 				 NULL,
 				 &valueType,
-				 dataBuffer,
+				 (LPBYTE) dataBuffer,
 				 &dataSize);
 	    } else {
 		break;
@@ -16569,17 +16690,23 @@
 		    retVal = nil;
 		    break;
 
-		case REG_BINARY:
-		    retVal = __MKBYTEARRAY(dataBuffer ? dataBuffer : quickData.smallDataBuffer, dataSize);
+		case REG_BINARY:      
+		    retVal = __MKBYTEARRAY(dataBuffer ? dataBuffer : quickData.wstringBuffer, dataSize);     
 		    break;
 
 		case REG_SZ:
 		case REG_EXPAND_SZ:
-#ifdef USE_UNICODE
-		    retVal = __MKU16STRING(dataBuffer ? dataBuffer : quickData.smallDataBuffer);
-#else
-		    retVal = __MKSTRING(dataBuffer ? dataBuffer : quickData.smallDataBuffer);
-#endif
+
+#if 0
+            console_printf("dataBuffer before __MKU16STTRING: %S\n", dataBuffer);
+            console_printf("QuickData smallDataBuffer before __MKU16STTRING: %S\n", quickData.wstringBuffer);
+#endif            
+		    retVal = __MKU16STRING(dataBuffer ? dataBuffer : quickData.wstringBuffer);
+#if 0        
+            console_printf("QuickData smallDataBuffer: %S\n", quickData.wstringBuffer);
+            console_printf("dataBuffer: %S\n", dataBuffer);
+#endif            
+
 		    break;
 
 #if 0
@@ -16603,14 +16730,20 @@
 		    val = (val << 8) | quickData.dWordBytes[3];
 		    retVal = __MKUINT(val);
 		    break;
-
+        
+        case REG_QWORD: // only REG_QWORD_LITTLE_ENDIAN present (no need for shifts)
+            retVal = __MKULARGEINT(quickData.qWord);
+            break;
+           
 		case REG_MULTI_SZ:
 		    {
-			CHAR *cp, *cp0;
+			wchar_t *cp, *cp0;
 			int ns, i;
-
-			cp0 = dataBuffer ? dataBuffer : quickData.smallDataBuffer;
+ 
+            cp0 = dataBuffer ? dataBuffer : quickData.wstringBuffer;
+
 #if 0
+
 			console_printf("**************\n");
 			for (i=0;i<50;i++) {
 			  console_printf("%x ", cp0[i]);
@@ -16627,30 +16760,34 @@
 #endif
 			cp = cp0;
 			ns = 0;
-			while (*cp) {
-			    while (*cp++) ;;
-			    ns++;
+
+            // datasize
+            while ((((cp - cp0) * sizeof(wchar_t)) < (dataSize - 1)) // limits the string
+             && *cp) { // check if the dereferenced value is NULL 
+                cp = wcschr(cp, '\0'); // wide string search for terminating null character
+                *cp++;
+                ns++;
 			}
+
+            // clear any remaining value
+            *cp = '\0';
 			stringArray = __ARRAY_NEW_INT(ns);
 
 			i = 0;
 			while (*cp0) {
 			    OBJ s;
-			    CHAR *cp;
-
-			    cp = cp0;
-			    while (*cp++) ;;
-#ifdef USE_UNICODE
-			    s = __MKU16STRING(cp0); __ArrayInstPtr(stringArray)->a_element[i] = s; __STORE(stringArray, s);
-#else
-			    s = __MKSTRING(cp0); __ArrayInstPtr(stringArray)->a_element[i] = s; __STORE(stringArray, s);
-#endif
-			    cp0 = cp;
+    
+    	        cp = cp0;
+	            s = __MKU16STRING(cp0); __ArrayInstPtr(stringArray)->a_element[i] = s; __STORE(stringArray, s);
+                cp = wcschr(cp, '\0');
+                *cp++;
+                cp0 = cp;
 			    i++;
+                
 			}
 			retVal = stringArray;
-			break;
-		    }
+			break;             
+		    }       
 		default:
 		    console_printf("RegistryEntry [warning]: unhandled valueType: %d\n", valueType);
 		    break;
@@ -16660,9 +16797,10 @@
 	     && (ret != ERROR_FILE_NOT_FOUND)) {
 		errorNumber = __MKSMALLINT(ret);
 	    }
-	}
+	} 
     }
     if (dataBuffer) free(dataBuffer);
+
 %}.
     errorNumber notNil ifTrue:[
 	(OperatingSystem errorHolderForNumber:errorNumber) reportError.
@@ -16689,6 +16827,26 @@
 
     |data stringArray errorNumber|
 %{
+    /* Registry Element Size Limits
+     * 
+     * Found at MSDN: https://docs.microsoft.com/en-us/windows/desktop/SysInfo/registry-element-size-limits
+     * 
+     * Registry Element | Size Limit
+     * Key name           255 characters. The key name includes the absolute path of the key in the registry, always starting at a base key, for example, HKEY_LOCAL_MACHINE.
+     * Value name         16,383 characters; Windows 2000: 260 ANSI characters or 16,383 Unicode characters.
+     * Value              Available memory (latest format)1 MB (standard format)
+     * Tree               A registry tree can be 512 levels deep. You can create up to 32 levels at a time through a single registry API call.
+     * 
+     * Notes:
+     * - Long values (more than 2,048 bytes) should be stored in a file, and the location of the file should be stored in the registry. 
+     * This helps the registry perform efficiently.
+     * 
+     * - The file location can be the name of a value or the data of a value. Each backslash in the location string must be preceded by 
+     * another backslash as an escape character. For example, specify "C:\\mydir\\myfile" to store the string "C:\mydir\myfile". 
+     * A file location can also be the name of a key if it is within the 255-character limit for key names and does not contain backslashes,
+     * which are not allowed in key names.
+     */
+
     HKEY myKey;
     DWORD valueType = -1;
     int val;
@@ -16918,7 +17076,7 @@
     "evaluate aBlock for all value names"
 
     ^ Array streamContents:[:s |
-	self valueNamesDo:[:nm | s nextPut:nm]
+	self valueNamesDo:[:nm | s nextPutUnicode:nm]
     ].
 
     "Created: / 18-01-2011 / 20:24:52 / cg"