2011-12-21 09:55:05 +00:00
|
|
|
/*
|
|
|
|
Serval Distributed Numbering Architecture (DNA)
|
|
|
|
Copyright (C) 2010 Paul Gardner-Stephen
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
|
|
modify it under the terms of the GNU General Public License
|
|
|
|
as published by the Free Software Foundation; either version 2
|
|
|
|
of the License, or (at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program; if not, write to the Free Software
|
|
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
*/
|
|
|
|
|
2012-02-23 02:15:42 +00:00
|
|
|
#include "serval.h"
|
2011-12-18 21:40:02 +00:00
|
|
|
#include "rhizome.h"
|
2012-01-03 06:05:02 +00:00
|
|
|
#include <stdlib.h>
|
2011-12-13 09:04:12 +00:00
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
/* Import a bundle from the inbox folder. The bundle is contained a pair of files, one containing
|
|
|
|
the manifest and the optional other containing the payload.
|
|
|
|
|
|
|
|
The logic is all in rhizome_add_manifest(). This function just wraps that function and manages
|
|
|
|
file and object buffers and lifetimes.
|
2011-12-13 09:04:12 +00:00
|
|
|
*/
|
2012-04-02 08:12:40 +00:00
|
|
|
|
2012-05-14 06:02:28 +00:00
|
|
|
int rhizome_bundle_import(rhizome_manifest *m_in, rhizome_manifest **m_out, const char *bundle,
|
2012-04-13 08:17:20 +00:00
|
|
|
char *groups[], int ttl,
|
2011-12-20 21:16:12 +00:00
|
|
|
int verifyP, int checkFileP, int signP)
|
2011-12-13 09:04:12 +00:00
|
|
|
{
|
2012-04-13 08:17:20 +00:00
|
|
|
if (m_out) *m_out = NULL;
|
|
|
|
|
2011-12-13 09:04:12 +00:00
|
|
|
char filename[1024];
|
|
|
|
char manifestname[1024];
|
2012-05-14 06:02:28 +00:00
|
|
|
if (!FORM_RHIZOME_DATASTORE_PATH(filename, "import/file.%s", bundle)
|
|
|
|
|| !FORM_RHIZOME_DATASTORE_PATH(manifestname, "import/manifest.%s", bundle))
|
2012-04-02 08:12:40 +00:00
|
|
|
return WHY("Manifest bundle name too long");
|
2012-01-27 05:51:48 +00:00
|
|
|
|
2012-04-02 08:12:40 +00:00
|
|
|
/* Read manifest file if no manifest was given */
|
|
|
|
rhizome_manifest *m = m_in;
|
|
|
|
if (!m_in) {
|
|
|
|
m = rhizome_read_manifest_file(manifestname, 0 /* file not buffer */, RHIZOME_VERIFY);
|
|
|
|
if (!m)
|
|
|
|
return WHY("Could not read manifest file.");
|
|
|
|
} else {
|
|
|
|
if (debug&DEBUG_RHIZOMESYNC)
|
|
|
|
fprintf(stderr,"Importing direct from manifest structure hashP=%d\n",m->fileHashedP);
|
|
|
|
}
|
2011-12-13 09:04:12 +00:00
|
|
|
|
2012-04-02 08:12:40 +00:00
|
|
|
/* Add the manifest and its associated file to the Rhizome database. */
|
2012-04-13 08:17:20 +00:00
|
|
|
rhizome_manifest *dupm;
|
2012-05-15 07:54:25 +00:00
|
|
|
int ret = rhizome_add_manifest(m, &dupm, filename, groups, ttl,
|
|
|
|
verifyP, checkFileP, signP,
|
|
|
|
NULL /* don't specify author for manifests
|
|
|
|
received via mesh */);
|
2012-04-02 08:12:40 +00:00
|
|
|
unlink(filename);
|
|
|
|
if (ret == -1) {
|
|
|
|
unlink(manifestname);
|
|
|
|
} else {
|
|
|
|
/* >>> For testing, write manifest file back to disk and leave it there */
|
|
|
|
// unlink(manifestname);
|
|
|
|
if (rhizome_write_manifest_file(m, manifestname))
|
|
|
|
ret = WHY("Could not write manifest file.");
|
|
|
|
}
|
2012-01-27 05:51:48 +00:00
|
|
|
|
2012-04-13 08:17:20 +00:00
|
|
|
/* If the manifest structure was allocated in this function, and it is not being returned to the
|
|
|
|
caller, then this function is responsible for freeing it */
|
|
|
|
if (m_out)
|
|
|
|
*m_out = m;
|
|
|
|
else if (!m_in)
|
2012-04-02 08:12:40 +00:00
|
|
|
rhizome_manifest_free(m);
|
2011-12-13 09:04:12 +00:00
|
|
|
|
2012-04-02 08:12:40 +00:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
/* Add a manifest/payload pair ("bundle") to the rhizome data store.
|
|
|
|
|
2012-04-16 02:16:58 +00:00
|
|
|
Returns:
|
|
|
|
0 if successful
|
|
|
|
2 if a duplicate is already in the store (same name, version and filehash)
|
|
|
|
-1 on error or failure
|
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
Fills in any missing manifest fields (generating a new, random manifest ID if necessary),
|
|
|
|
optionally performs consistency checks (see below), adds the manifest to the given groups (for
|
|
|
|
which private keys must be held), optionally signs it, and inserts the manifest and payload into
|
|
|
|
the rhizome store unless the store already contains a manifest with the same ID and a higher
|
|
|
|
version number or an identical manifest (in which case the stored version number is bumped to the
|
|
|
|
maximum of the two).
|
|
|
|
|
|
|
|
This function is called in three different situations:
|
|
|
|
(a) when the user injects a file (with or without a complete manifest) into rhizome,
|
|
|
|
(b) when a manifest is received via the mesh and the file is already present in the rhizome store,
|
|
|
|
(c) when a file is received via the mesh for a manifest that was received previously.
|
|
|
|
|
|
|
|
The following arguments distinguish these situations:
|
|
|
|
|
|
|
|
ttl
|
|
|
|
- In case (a) ttl will be typically set to the initial (maximum?) TTL for a manifest. In
|
|
|
|
cases (b) and (c) ttl will be the TTL of the received manifest decremented by one. This
|
|
|
|
function clamps the supplied value into the legal range 0..255, so callers need not perform
|
|
|
|
range checking after decrement.
|
|
|
|
|
|
|
|
verifyP
|
|
|
|
- If set, then checks that no signature verifications failed when the manifest was loaded.
|
|
|
|
(If checkFileP is given, then also checks that the payload and manifest are consistent.)
|
|
|
|
This is used in case (a) if the user provided a manifest file, and always for cases (b) and
|
|
|
|
(c). It ensures the integrity of the received/provided manifest, and ensures that the
|
|
|
|
received/provided payload is actually the one that the manifest belongs to. If verifyP is
|
|
|
|
false, then the new manifest will be overwritten with the correct values for the payload.
|
|
|
|
|
|
|
|
checkFileP
|
|
|
|
- If set then checks that the payload file is readable, and will cause verifyP to also check
|
|
|
|
that the payload matches the values in the manifest, specifically length and content hash.
|
|
|
|
This is always used in cases (a) and (c), but not in case (b) because in that case, the
|
|
|
|
file's contents with the given file hash are known to be already in the database.
|
|
|
|
|
|
|
|
signP
|
|
|
|
- If set, then signs the manifest after all its fields have been filled in. Only used in case
|
|
|
|
(a), because in cases (b) and (c) the manifest has already been signed, since it is already
|
|
|
|
on the air.
|
|
|
|
|
|
|
|
A bundle can either be an ordinary manifest-payload pair, or a group description.
|
|
|
|
|
|
|
|
- Group descriptions are manifests with no payload that have the "isagroup" variable set. They
|
|
|
|
get stored in the manifests table AND a reference is added to the grouplist table. Any
|
|
|
|
manifest, including any group manifest, may be a member of zero or one group. This allows a
|
|
|
|
nested, i.e., multi-level group hierarchy where sub-groups will only typically be discovered
|
|
|
|
by joining the parent group.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
2012-04-13 08:17:20 +00:00
|
|
|
int rhizome_add_manifest(rhizome_manifest *m_in,
|
|
|
|
rhizome_manifest **m_out,
|
2012-04-11 09:10:10 +00:00
|
|
|
const char *filename,
|
|
|
|
char *groups[],
|
|
|
|
int ttl,
|
|
|
|
int verifyP, // verify that file's hash is consistent with manifest
|
|
|
|
int checkFileP,
|
2012-05-15 07:54:25 +00:00
|
|
|
int signP,
|
|
|
|
const char *author
|
2012-04-11 09:10:10 +00:00
|
|
|
)
|
2012-04-02 08:12:40 +00:00
|
|
|
{
|
2012-04-13 08:17:20 +00:00
|
|
|
if (m_out) *m_out = NULL;
|
2012-01-03 06:05:02 +00:00
|
|
|
|
2012-04-12 09:00:52 +00:00
|
|
|
/* Ensure manifest meets basic sanity checks. */
|
2012-04-13 08:17:20 +00:00
|
|
|
const char *name = rhizome_manifest_get(m_in, "name", NULL, 0);
|
2012-04-12 09:00:52 +00:00
|
|
|
if (name == NULL || !name[0])
|
|
|
|
return WHY("Manifest missing 'name' field");
|
2012-04-13 08:17:20 +00:00
|
|
|
if (rhizome_manifest_get_ll(m_in, "date") == -1)
|
2012-04-12 09:00:52 +00:00
|
|
|
return WHY("Manifest missing 'date' field");
|
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
/* Keep payload file name handy for later */
|
2012-04-13 08:17:20 +00:00
|
|
|
m_in->dataFileName = strdup(filename);
|
2012-04-02 08:12:40 +00:00
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
/* Store time to live, clamped to within legal range */
|
2012-04-13 08:17:20 +00:00
|
|
|
m_in->ttl = ttl < 0 ? 0 : ttl > 254 ? 254 : ttl;
|
2012-04-02 08:12:40 +00:00
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
/* Check payload file is accessible and discover its length, then check that it matches
|
|
|
|
the file size stored in the manifest */
|
|
|
|
if (checkFileP) {
|
2012-01-27 05:51:48 +00:00
|
|
|
struct stat stat;
|
2012-04-02 08:12:40 +00:00
|
|
|
if (lstat(filename,&stat))
|
2012-04-11 09:10:10 +00:00
|
|
|
return WHY("Could not stat() payload file");
|
2012-04-13 08:17:20 +00:00
|
|
|
m_in->fileLength = stat.st_size;
|
|
|
|
long long mfilesize = rhizome_manifest_get_ll(m_in, "filesize");
|
|
|
|
if (mfilesize != -1 && mfilesize != m_in->fileLength) {
|
|
|
|
WHYF("Manifest.filesize (%lld) != actual file size (%lld)", mfilesize, m_in->fileLength);
|
|
|
|
if (verifyP)
|
|
|
|
return -1;
|
2012-04-12 09:00:52 +00:00
|
|
|
}
|
2011-12-20 00:55:52 +00:00
|
|
|
}
|
|
|
|
|
2012-04-13 08:17:20 +00:00
|
|
|
/* Bail out now if errors occurred loading the manifest file, eg signature failed to validate */
|
|
|
|
if (verifyP && m_in->errors)
|
|
|
|
return WHYF("Manifest.errors (%d) is non-zero", m_in->errors);
|
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
/* Compute hash of payload unless we know verification has already failed */
|
2012-04-13 08:17:20 +00:00
|
|
|
if (checkFileP || signP) {
|
|
|
|
char hexhash[SHA512_DIGEST_STRING_LENGTH];
|
2012-04-02 08:12:40 +00:00
|
|
|
if (rhizome_hash_file(filename, hexhash))
|
2012-03-13 08:01:29 +00:00
|
|
|
return WHY("Could not hash file.");
|
2012-04-13 08:17:20 +00:00
|
|
|
memcpy(&m_in->fileHexHash[0], &hexhash[0], SHA512_DIGEST_STRING_LENGTH);
|
|
|
|
m_in->fileHashedP = 1;
|
2011-12-13 09:04:12 +00:00
|
|
|
}
|
|
|
|
|
2012-04-13 08:17:20 +00:00
|
|
|
/* Check that payload hash matches manifest */
|
2012-04-11 09:10:10 +00:00
|
|
|
if (checkFileP) {
|
2012-04-13 08:17:20 +00:00
|
|
|
const char *mhexhash = rhizome_manifest_get(m_in, "filehash", NULL, 0);
|
|
|
|
if (mhexhash && strcmp(m_in->fileHexHash, mhexhash)) {
|
|
|
|
WHYF("Manifest.filehash (%s) does not match payload hash (%s)", mhexhash, m_in->fileHexHash);
|
|
|
|
if (verifyP)
|
|
|
|
return -1;
|
2012-04-12 09:00:52 +00:00
|
|
|
}
|
2012-04-11 09:10:10 +00:00
|
|
|
}
|
|
|
|
|
2012-04-12 09:00:52 +00:00
|
|
|
/* Fill in the manifest so that duplicate detection can be performed, and to avoid redundant work
|
|
|
|
by rhizome_manifest_finalise() below. */
|
2012-04-11 09:10:10 +00:00
|
|
|
if (checkFileP) {
|
2012-04-13 08:17:20 +00:00
|
|
|
rhizome_manifest_set(m_in, "filehash", m_in->fileHexHash);
|
|
|
|
rhizome_manifest_set_ll(m_in, "first_byte", 0);
|
|
|
|
rhizome_manifest_set_ll(m_in, "last_byte", m_in->fileLength);
|
2011-12-13 09:04:12 +00:00
|
|
|
}
|
2012-04-11 09:10:10 +00:00
|
|
|
|
2012-04-16 02:16:58 +00:00
|
|
|
/* Make sure the manifest structure contains the version number, which may legitimately be -1 if
|
|
|
|
the caller did not provide a version. */
|
|
|
|
m_in->version = rhizome_manifest_get_ll(m_in, "version");
|
|
|
|
|
2012-04-12 09:00:52 +00:00
|
|
|
/* Check if a manifest is already stored for the same payload with the same details.
|
|
|
|
This catches the case of "dna rhizome add file <filename>" on the same file more than once.
|
|
|
|
(Debounce!) */
|
|
|
|
rhizome_manifest *dupm = NULL;
|
2012-04-13 08:17:20 +00:00
|
|
|
if (rhizome_find_duplicate(m_in, &dupm) == -1)
|
2012-04-12 09:00:52 +00:00
|
|
|
return WHY("Errors encountered searching for duplicate manifest");
|
|
|
|
if (dupm) {
|
2012-04-13 08:17:20 +00:00
|
|
|
if (debug & DEBUG_RHIZOME)
|
2012-04-16 02:16:58 +00:00
|
|
|
fprintf(stderr, "Found duplicate payload: name=\"%s\" version=%llu hexhash=%s -- not adding\n", name, dupm->version, dupm->fileHexHash);
|
|
|
|
/* If the caller wants the duplicate manifest, it must be finalised, otherwise discarded. */
|
2012-04-13 08:17:20 +00:00
|
|
|
if (m_out) {
|
2012-05-15 10:34:41 +00:00
|
|
|
if (rhizome_manifest_finalise(dupm, 0,NULL))
|
2012-04-13 08:17:20 +00:00
|
|
|
return WHY("Failed to finalise manifest.\n");
|
|
|
|
*m_out = dupm;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
rhizome_manifest_free(dupm);
|
2012-04-16 02:16:58 +00:00
|
|
|
return 2;
|
2012-04-12 09:00:52 +00:00
|
|
|
}
|
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
/* Supply manifest version number if missing, so we can do the version check below */
|
2012-04-16 02:16:58 +00:00
|
|
|
if (m_in->version == -1) {
|
2012-05-11 01:08:46 +00:00
|
|
|
m_in->version = gettime_ms();
|
2012-04-16 02:16:58 +00:00
|
|
|
rhizome_manifest_set_ll(m_in, "version", m_in->version);
|
2012-04-11 09:10:10 +00:00
|
|
|
}
|
|
|
|
|
2012-04-13 08:17:20 +00:00
|
|
|
/* If the manifest already has an ID */
|
|
|
|
char *id = NULL;
|
|
|
|
if ((id = rhizome_manifest_get(m_in, "id", NULL, 0))) {
|
2012-04-16 02:16:58 +00:00
|
|
|
/* Discard the new manifest unless it is newer than the most recent known version with the same ID */
|
2012-04-11 09:10:10 +00:00
|
|
|
long long storedversion = sqlite_exec_int64("SELECT version from manifests where id='%s';", id);
|
2012-04-16 02:16:58 +00:00
|
|
|
if (debug & DEBUG_RHIZOME)
|
|
|
|
fprintf(stderr, "Found existing version=%lld, new version=%lld\n", storedversion, m_in->version);
|
|
|
|
if (m_in->version < storedversion) {
|
2012-04-11 09:10:10 +00:00
|
|
|
return WHY("Newer version exists");
|
|
|
|
}
|
2012-04-16 02:16:58 +00:00
|
|
|
if (m_in->version == storedversion) {
|
|
|
|
return WHY("Same version exists");
|
|
|
|
}
|
2012-04-13 08:17:20 +00:00
|
|
|
/* Check if we know its private key */
|
|
|
|
rhizome_hex_to_bytes(id, m_in->cryptoSignPublic, crypto_sign_edwards25519sha512batch_PUBLICKEYBYTES*2);
|
2012-05-15 10:34:41 +00:00
|
|
|
if (!rhizome_extract_privatekey(m_in,author))
|
2012-04-13 08:17:20 +00:00
|
|
|
m_in->haveSecret=1;
|
2012-04-11 09:10:10 +00:00
|
|
|
} else {
|
|
|
|
/* The manifest had no ID (256 bit random string being a public key in the NaCl CryptoSign
|
|
|
|
crypto system), so create one. */
|
2012-04-13 08:17:20 +00:00
|
|
|
rhizome_manifest_createid(m_in);
|
2012-04-11 09:10:10 +00:00
|
|
|
/* The ID is implicit in transit, but we need to store it in the file, so that reimporting
|
|
|
|
manifests on receiver nodes works easily. We might implement something that strips the id
|
|
|
|
variable out of the manifest when sending it, or some other scheme to avoid sending all the
|
2012-05-15 07:54:25 +00:00
|
|
|
extra bytes. */
|
2012-04-13 08:17:20 +00:00
|
|
|
id = rhizome_bytes_to_hex(m_in->cryptoSignPublic, crypto_sign_edwards25519sha512batch_PUBLICKEYBYTES);
|
|
|
|
rhizome_manifest_set(m_in, "id", id);
|
2012-05-15 07:54:25 +00:00
|
|
|
if (author) {
|
|
|
|
/* Set the BK using the provided authorship information.
|
|
|
|
Serval Security Framework defines BK as being:
|
|
|
|
BK = privateKey XOR sha512(RS##BID), where BID = cryptoSignPublic,
|
|
|
|
and RS is the rhizome secret for the specified author.
|
|
|
|
The nice thing about this specification is that:
|
|
|
|
privateKey = BK XOR sha512(RS##BID), so the same function can be used
|
|
|
|
to encrypt and decrypt the BK field. */
|
2012-05-15 10:34:41 +00:00
|
|
|
int len=crypto_sign_edwards25519sha512batch_SECRETKEYBYTES;
|
|
|
|
unsigned char bkbytes[len];
|
|
|
|
if (!rhizome_bk_xor(author,m_in->cryptoSignPublic,
|
|
|
|
m_in->cryptoSignPublic,
|
|
|
|
bkbytes)) {
|
2012-05-15 12:45:13 +00:00
|
|
|
WHYF("set BK='%s'",rhizome_bytes_to_hex(bkbytes,len));
|
2012-05-15 10:34:41 +00:00
|
|
|
rhizome_manifest_set(m_in,"BK",rhizome_bytes_to_hex(bkbytes,len));
|
2012-05-15 12:45:13 +00:00
|
|
|
} else {
|
|
|
|
WHY("Failed to set BK");
|
2012-05-15 10:34:41 +00:00
|
|
|
}
|
2012-05-15 07:54:25 +00:00
|
|
|
}
|
2012-04-11 09:10:10 +00:00
|
|
|
}
|
|
|
|
|
2011-12-13 09:04:12 +00:00
|
|
|
/* Add group memberships */
|
2012-04-02 08:12:40 +00:00
|
|
|
if (groups) {
|
|
|
|
int i;
|
|
|
|
for(i = 0; groups[i]; i++)
|
2012-04-13 08:17:20 +00:00
|
|
|
rhizome_manifest_add_group(m_in, groups[i]);
|
2011-12-20 00:55:52 +00:00
|
|
|
}
|
2011-12-13 09:04:12 +00:00
|
|
|
|
2012-04-11 09:10:10 +00:00
|
|
|
/* Finish completing the manifest */
|
2012-05-15 10:34:41 +00:00
|
|
|
if (rhizome_manifest_finalise(m_in, signP,author))
|
2012-04-02 08:12:40 +00:00
|
|
|
return WHY("Failed to finalise manifest.\n");
|
2011-12-13 09:04:12 +00:00
|
|
|
|
|
|
|
/* Okay, it is written, and can be put directly into the rhizome database now */
|
2012-04-13 08:17:20 +00:00
|
|
|
if (rhizome_store_bundle(m_in, filename) == -1)
|
2012-04-02 08:12:40 +00:00
|
|
|
return WHY("rhizome_store_bundle() failed.");
|
2011-12-13 09:04:12 +00:00
|
|
|
|
2012-05-11 21:54:52 +00:00
|
|
|
monitor_announce_bundle(m_in);
|
2012-04-13 08:17:20 +00:00
|
|
|
if (m_out) *m_out = m_in;
|
2012-04-02 08:12:40 +00:00
|
|
|
return 0;
|
2011-12-13 09:04:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Update an existing Rhizome bundle */
|
|
|
|
int rhizome_bundle_push_update(char *id,long long version,unsigned char *data,int appendP)
|
|
|
|
{
|
|
|
|
return WHY("Not implemented");
|
|
|
|
}
|
|
|
|
|
2011-12-20 05:18:26 +00:00
|
|
|
char nybltochar(int nybl)
|
|
|
|
{
|
|
|
|
if (nybl<0) return '?';
|
|
|
|
if (nybl>15) return '?';
|
|
|
|
if (nybl<10) return '0'+nybl;
|
|
|
|
return 'A'+nybl-10;
|
|
|
|
}
|
|
|
|
|
|
|
|
int chartonybl(int c)
|
|
|
|
{
|
|
|
|
if (c>='A'&&c<='F') return 0x0a+(c-'A');
|
|
|
|
if (c>='a'&&c<='f') return 0x0a+(c-'a');
|
2011-12-20 06:57:24 +00:00
|
|
|
if (c>='0'&&c<='9') return 0x00+(c-'0');
|
2011-12-20 05:18:26 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2012-05-02 06:33:09 +00:00
|
|
|
int rhizome_hex_to_bytes(const char *in,unsigned char *out,int hexChars)
|
2011-12-20 05:18:26 +00:00
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
for(i=0;i<hexChars;i++)
|
|
|
|
{
|
|
|
|
int byte=i>>1;
|
|
|
|
int nybl=chartonybl(in[i]);
|
|
|
|
out[byte]=out[byte]<<4;
|
|
|
|
out[byte]|=nybl;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
2011-12-20 06:57:24 +00:00
|
|
|
|