Some curious things with sqlite3_column_bytes

I am using SQLCipher in Swift/iOS development. I found a curious thing : The length returned by sqlite3_column_bytes function will be limited to 255. The corresponding column was TEXT field and had at least 1024 characters. I tried sqlite3_column_bytes16 to check the length, the return result was 510 (also limited). I used DataGrip (with sqlcihper-jdbc) to operate the field. It was totally correct. So what was wrong ?

By the way, the text had no ‘\0’. All are ASCII characters in JSON format.

Hi @forestluo

Thanks for your interest in SQLCipher and for reaching out.

I just re-tested inserting/fetching non-null terminated strings using SQLCipher on iOS Swift and was properly getting back the correct values back when using lengths >= 1024.

Because of that my preliminary inclination is that this is an implementation issue.

How are you inserting the data? If it’s non-null terminated you’ll need to pass an explicit length value to sqlite3_bind_text(...)

Are you calling sqlite3_column_text(...) before calling sqlite3_column_bytes(...) as recommended as the safest policy in the SQLite documentation here: Result Values From A Query ?

If you’re using SQLCipher commercial/enterprise edition, please feel free to write us at support@zetetic.net for private prioritized support.

If you’re using community edition, I’d recommend outlining how you’re integrating SQLCipher (i.e. Swift Package Manager, amalgamation, CocoaPods) and posting up a code snippet/example here which is reproducing the problem to see if anyone from the community can assist.

Thanks for your reply at first.

I am using SQLCipher 4.16.0 community version, which was imported into my project by Xcode (File → Add Package Dependencies).

I am loading header, databody, and remark from datatables. At first, I used sqlite3_column_text function,which return a cut string with 255 octets for data body. So I changed to sqlite3_column_bytes, which directly returned 255 for data body.

I am using SQLite.swift package also. It returns correct text from a SQLite database (without sqlcipher).

The data already exists in SQLite. I can browse it with DataGrip(with sqlcipher-jdbc driver). The length of data body exceeds 255 in UTF8.

Hi @forestluo

Thanks for providing the sample. I still wasn’t able to reproduce the issue in an example program using your swift code for stepping.

Sample code:

import SQLCipher

enum SQLCipherError: Error {
    case openFailure
    case keyFailure
    case createTableFailure
    case prepareFailure
    case bindFailure
    case stepFailure
}

struct SQLCipherManager {

	static func testSQLCipherColumnBytes(_ charCount: Int32) throws {
	    var rc: Int32
	    var db: OpaquePointer? = nil
	    var stmt: OpaquePointer? = nil
	    let password: String = "correct horse battery staple"
	    var thrownError: Error?
	    do {
	        guard let dbPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("sqlcipher.db").path() else {
	            throw SQLCipherError.openFailure
	        }
	        rc = sqlite3_open(dbPath, &db)
	        guard rc == SQLITE_OK else {
	            let errMsg = String(cString: sqlite3_errmsg(db))
	            print("sqlite3 error \(errMsg)")
	            throw SQLCipherError.openFailure
	        }
	        
	        rc = sqlite3_key(db, password, Int32(password.utf8CString.count))
	        guard rc == SQLITE_OK else {
	            throw SQLCipherError.keyFailure
	        }
	        
	        rc = sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS datatables(id INTEGER PRIMARY KEY AUTOINCREMENT, header TEXT, databody TEXT, remark TEXT);", nil, nil, nil)
	        guard rc == SQLITE_OK else {
	            throw SQLCipherError.createTableFailure
	        }
	        
	        rc = sqlite3_prepare(db, "INSERT INTO datatables(header, databody, remark) VALUES(?,?,?) returning id;", -1, &stmt, nil)
	        guard rc == SQLITE_OK else {
	            throw SQLCipherError.prepareFailure
	        }
	        
	        let repeatedCString = String(repeating: "a", count: Int(charCount)).withCString { cString in
	            return cString
	        }
	        // only use the exact char count without null termination
	        rc = sqlite3_bind_text(stmt, 1, repeatedCString, charCount, SQLITE_TRANSIENT)
	        guard rc == SQLITE_OK else {
	            throw SQLCipherError.bindFailure
	        }
	        rc = sqlite3_bind_text(stmt, 2, repeatedCString, charCount, SQLITE_TRANSIENT)
	        guard rc == SQLITE_OK else {
	            throw SQLCipherError.bindFailure
	        }
	        rc = sqlite3_bind_text(stmt, 3, repeatedCString, charCount, SQLITE_TRANSIENT)
	        guard rc == SQLITE_OK else {
	            throw SQLCipherError.bindFailure
	        }
	        
	        var id: Int32
	        if sqlite3_step(stmt) == SQLITE_ROW {
	            id = sqlite3_column_int(stmt, 0)
	        } else {
	            throw SQLCipherError.stepFailure
	        }
	        
	        sqlite3_finalize(stmt)
	        
	        rc = sqlite3_prepare(db, "SELECT header, databody, remark FROM datatables WHERE id = ?;", -1, &stmt, nil)
	        guard rc == SQLITE_OK else {
	            throw SQLCipherError.prepareFailure
	        }
	        
	        rc = sqlite3_bind_int(stmt, 1, id)
	        
	        while sqlite3_step(stmt) == SQLITE_ROW {
	            let columns: [String?] = (0...2).map { index in
	                let len = sqlite3_column_bytes(stmt, Int32(index))
	                print("length = \(len)")
	                if len > 0,
	                   let ptr = sqlite3_column_blob(stmt, Int32(len)) {
	                    let data = Data(bytes: ptr, count: Int(len))
	                    return String(data: data, encoding: .utf8)
	                } else { return nil }
	            }
	        }
	        
	        sqlite3_finalize(stmt)
	        
	        rc = sqlite3_prepare(db, "SELECT octet_length(header), octet_length(databody), octet_length(remark) FROM datatables WHERE id = ?;", -1, &stmt, nil)
	        guard rc == SQLITE_OK else {
	            throw SQLCipherError.prepareFailure
	        }
	        
	        rc = sqlite3_bind_int(stmt, 1, id)
	        
	        while sqlite3_step(stmt) == SQLITE_ROW {
	            let vals: [Int32?] = (0...2).map { index in
	                let val = sqlite3_column_int(stmt, Int32(index))
	                print("octet_length = \(val)")
	                return val
	            }
	        }
	    } catch {
	        thrownError = error
	    }
	    if stmt != nil {
	        sqlite3_finalize(stmt)
	    }
	    if db != nil {
	        sqlite3_close(db)
	    }
	    if let err = thrownError {
	        throw err
	    }
	}

}

Caller example:

try SQLCipherManager.testSQLCipherColumnBytes(4096)

Which prints out:

length = 4096
length = 4096
length = 4096
octet_length = 4096
octet_length = 4096
octet_length = 4096

A few other questions:

  1. Could you try running this query in your project on your data to see what it returns for the octet_length:
"SELECT octet_length(header), octet_length(databody), octet_length(remark) FROM datatables WHERE id = ?;"
  1. What does your table schema look like? Can you show the sample code of how you’re inserting the data?
  2. Are you able to provide a non-sensitive example of column content which reproduces the issue?
  3. Can you try the sample function above in your project and see if it produces the expected result?
  4. Are you able to reproduce the issue in a standalone sample project with only a SQLCipher depdendency (no other sqlite dependencies)? If so can you share the sample project?