[DBD::Pg 1/8] Fix UTF-8 support in placeholders and return values

[email protected] Wed, 21 May 2014 15:35:10 +0000
Newsgroups perl.dbd.pg.changes
Message-ID <[email protected]>
Committed by =?UTF-8?q?Dagfinn=20Ilmari=20Manns=C3=A5ker?= <[email protected]>

Subject: [DBD::Pg 1/8] Fix UTF-8 support in placeholders and return values

---
 Pg.xs         |  2 +-
 dbdimp.c      | 53 +++++++++++++++++++++++++++++++++++++++++++++-----
 dbdimp.h      |  4 +++-
 t/02attribs.t |  2 +-
 t/30unicode.t | 62 ++++++++++++++++++++++++++++++++++-------------------------
 5 files changed, 89 insertions(+), 34 deletions(-)

diff --git a/Pg.xs b/Pg.xs
index edec91e..60f843b 100644
--- a/Pg.xs
+++ b/Pg.xs
@@ -224,7 +224,7 @@ quote(dbh, to_quote_sv, type_sv=Nullsv)
 		if (SvROK(to_quote_sv) && !SvAMAGIC(to_quote_sv)) {
 			if (SvTYPE(SvRV(to_quote_sv)) != SVt_PVAV)
 				croak("Cannot quote a reference");
-			to_quote_sv = pg_stringify_array(to_quote_sv, ",", imp_dbh->pg_server_version);
+			to_quote_sv = pg_stringify_array(to_quote_sv, ",", imp_dbh->pg_server_version, imp_dbh->client_encoding_utf8);
 		}
 
 		/* Null is always returned as "NULL", so we can ignore any type given */
diff --git a/dbdimp.c b/dbdimp.c
index 476eae7..fed72bd 100644
--- a/dbdimp.c
+++ b/dbdimp.c
@@ -2446,7 +2446,7 @@ int dbd_bind_ph (SV * sth, imp_sth_t * imp_sth, SV * ph_name, SV * newvalue, IV
 		}
 		else if (SvTYPE(SvRV(newvalue)) == SVt_PVAV) {
 			SV * quotedval;
-			quotedval = pg_stringify_array(newvalue,",",imp_dbh->pg_server_version);
+			quotedval = pg_stringify_array(newvalue,",",imp_dbh->pg_server_version,imp_dbh->client_encoding_utf8);
 			currph->valuelen = sv_len(quotedval);
 			Renew(currph->value, currph->valuelen+1, char); /* freed in dbd_st_destroy */
 			Copy(SvUTF8(quotedval) ? SvPVutf8_nolen(quotedval) : SvPV_nolen(quotedval),
@@ -2544,6 +2544,8 @@ int dbd_bind_ph (SV * sth, imp_sth_t * imp_sth, SV * ph_name, SV * newvalue, IV
 	(void)SvUPGRADE(newvalue, SVt_PV);
 
 	if (SvOK(newvalue)) {
+		/* get the right encoding, without modifying the caller's copy */
+		newvalue = pg_rightgraded_sv(aTHX_ newvalue, imp_dbh->client_encoding_utf8 && PG_BYTEA!=currph->bind_type->type_id);
 		value_string = SvPV(newvalue, currph->valuelen);
 		Renew(currph->value, currph->valuelen+1, char); /* freed in dbd_st_destroy */
 		Copy(value_string, currph->value, currph->valuelen, char);
@@ -2582,7 +2584,7 @@ int dbd_bind_ph (SV * sth, imp_sth_t * imp_sth, SV * ph_name, SV * newvalue, IV
 
 
 /* ================================================================== */
-SV * pg_stringify_array(SV *input, const char * array_delim, int server_version) {
+SV * pg_stringify_array(SV *input, const char * array_delim, int server_version, bool utf8) {
 
 	dTHX;
 	AV * toparr;
@@ -2602,6 +2604,8 @@ SV * pg_stringify_array(SV *input, const char * array_delim, int server_version)
 
 	toparr = (AV *) SvRV(input);
 	value = newSVpv("{", 1);
+	if (utf8)
+	    SvUTF8_on(value);
 
 	/* Empty arrays are easy */
 	if (av_len(toparr) < 0) {
@@ -2685,8 +2689,8 @@ SV * pg_stringify_array(SV *input, const char * array_delim, int server_version)
 				}
 				else {
 					sv_catpv(value, "\"");
-					if (SvUTF8(svitem))
-						SvUTF8_on(value);
+					/* avoid up- or down-grading the caller's value */
+					svitem = pg_rightgraded_sv(aTHX_ svitem, utf8);
 					string = SvPV(svitem, stringlength);
 					while (stringlength--) {
 						/* Escape backslashes and double-quotes. */
@@ -2872,6 +2876,41 @@ static SV * pg_destringify_array(pTHX_ imp_dbh_t *imp_dbh, unsigned char * input
 
 } /* end of pg_destringify_array */
 
+SV * pg_upgraded_sv(pTHX_ SV *input) {
+	U8 *p, *end;
+	STRLEN len;
+	/* SvPV() can change the value SvUTF8() (for overloaded values and tied values). */
+	p = (U8*)SvPV(input, len);
+	if(SvUTF8(input)) return input;
+	for(end = p + len; p != end; p++) {
+		if(*p & 0x80) {
+			SV *output = sv_mortalcopy(input);
+			sv_utf8_upgrade(output);
+			return output;
+		}
+	}
+	return input;
+}
+
+SV * pg_downgraded_sv(pTHX_ SV *input) {
+	U8 *p, *end;
+	STRLEN len;
+	/* SvPV() can change the value SvUTF8() (for overloaded values and tied values). */
+	p = (U8*)SvPV(input, len);
+	if(!SvUTF8(input)) return input;
+	for(end = p + len; p != end; p++) {
+		if(*p & 0x80) {
+			SV *output = sv_mortalcopy(input);
+			sv_utf8_downgrade(output, DBDPG_FALSE);
+			return output;
+		}
+	}
+	return input;
+}
+
+SV * pg_rightgraded_sv(pTHX_ SV *input, bool utf8) {
+	return utf8 ? pg_upgraded_sv(aTHX_ input) : pg_downgraded_sv(aTHX_ input);
+}
 
 /* ================================================================== */
 int pg_quickexec (SV * dbh, const char * sql, const int asyncflag)
@@ -3652,7 +3691,11 @@ AV * dbd_st_fetch (SV * sth, imp_sth_t * imp_sth)
 				if (type_info && PG_BYTEA == type_info->type_id) {
 					SvUTF8_off(sv);
 				}
-				else {
+				/*
+				  Don't try to upgrade references (e.g. arrays).
+				  pg_destringify_array() upgrades the items as appropriate.
+				*/
+				else if (!SvROK(sv)) {
 					SvUTF8_on(sv);
 				}
 			}
diff --git a/dbdimp.h b/dbdimp.h
index 350f1a3..abc082a 100644
--- a/dbdimp.h
+++ b/dbdimp.h
@@ -197,7 +197,9 @@ int pg_db_getfd (imp_dbh_t * imp_dbh);
 
 SV * pg_db_pg_notifies (SV *dbh, imp_dbh_t *imp_dbh);
 
-SV * pg_stringify_array(SV * input, const char * array_delim, int server_version);
+SV * pg_rightgraded_sv(pTHX_ SV *input, bool utf8);
+
+SV * pg_stringify_array(SV * input, const char * array_delim, int server_version, bool utf8);
 
 int pg_quickexec (SV *dbh, const char *sql, const int asyncflag);
 
diff --git a/t/02attribs.t b/t/02attribs.t
index e671fd5..5f43432 100644
--- a/t/02attribs.t
+++ b/t/02attribs.t
@@ -438,7 +438,7 @@ SKIP: {
 	ok (Encode::is_utf8($name), $t);
 
 	$t='Unicode (utf8) data returned from database is not corrupted';
-	is (length($name), 4, $t);
+	is ($name, $utf8_str, $t);
 
 	$t='ASCII text returned from database does have utf8 bit set';
 	$sth->finish();
diff --git a/t/30unicode.t b/t/30unicode.t
index 7c4da06..ad45fc5 100644
--- a/t/30unicode.t
+++ b/t/30unicode.t
@@ -23,41 +23,51 @@ if (! $dbh) {
 
 isnt ($dbh, undef, 'Connect to database for unicode testing');
 
-my $pgversion = $dbh->{pg_server_version};
-
 my $t;
 
-my $name = "\N{LATIN CAPITAL LETTER E WITH ACUTE}milie du Ch\N{LATIN SMALL LETTER A WITH CIRCUMFLEX}telet";
-
-my $SQL = 'SELECT ?::text';
-my $sth = $dbh->prepare($SQL);
-$sth->execute($name);
-my $result = $sth->fetchall_arrayref->[0][0];
-$t = 'Fetching UTF-8 string from the database returns proper string';
-is ($result, $name, $t);
-$t = 'Fetching UTF-8 string from the database returns string with UTF-8 flag on';
-ok (utf8::is_utf8($result), $t);
-
-$dbh->{pg_enable_utf8} = 0;
-$sth->execute($name);
-$result = $sth->fetchall_arrayref->[0][0];
-$t = 'Fetching UTF-8 string from the database returns proper string (pg_enable_utf8=0)';
-my $noutfname  = $name;
-Encode::_utf8_off($noutfname);
-is ($result, $noutfname, $t);
-$t = 'Fetching UTF-8 string from the database returns string with UTF-8 flag off (pg_enable_utf8=0)';
-ok (!utf8::is_utf8($result), $t);
+my $name_d = my $name_u = "\N{LATIN CAPITAL LETTER E WITH ACUTE}milie du Ch\N{LATIN SMALL LETTER A WITH CIRCUMFLEX}telet";
+utf8::downgrade($name_d);
+utf8::upgrade($name_u);
+
+foreach (
+    [upgraded => text => $name_u],
+    [downgraded => text => $name_d],
+    [upgraded => 'text[]' => [$name_u]],
+    [downgraded => 'text[]' => [$name_d]],
+    [mixed => 'text[]' => [$name_d,$name_u]],
+) {
+    my ($state, $type, $value) = @$_;
+    $dbh->{pg_enable_utf8} = 1;
+
+    my $SQL = "SELECT ?::$type";
+    my $sth = $dbh->prepare($SQL);
+    $sth->execute($value);
+    my $result = $sth->fetchall_arrayref->[0][0];
+    $t = "Fetching $state UTF-8 $type from the database returns proper value";
+    is_deeply ($result, $value, $t);
+    $t = "Fetching $state UTF-8 $type from the database returns string with UTF-8 flag on";
+    ok (utf8::is_utf8($_), $t) for (ref $result ? @{$result} : $result);
+
+    $dbh->{pg_enable_utf8} = 0;
+    $sth->execute($value);
+    $result = $sth->fetchall_arrayref->[0][0];
+    $t = "Fetching $state UTF-8 $type from the database returns proper string (pg_enable_utf8=0)";
+    utf8::encode($_) for (ref $value ? @{$value} : $value);
+    is_deeply ($result, $value, $t);
+    $t = "Fetching $state UTF-8 $type from the database returns string with UTF-8 flag off (pg_enable_utf8=0)";
+    ok (!utf8::is_utf8($result), $t) for (ref $result ? @{$result} : $result);
+}
 
 $t = 'Generated string is not utf8';
-$name = 'Ada Lovelace';
+my $name = 'Ada Lovelace';
 utf8::encode($name);
 ok (!utf8::is_utf8($name), $t);
 
 $dbh->{pg_enable_utf8} = -1;
-$SQL = 'SELECT ?::text';
-$sth = $dbh->prepare($SQL);
+my $SQL = 'SELECT ?::text';
+my $sth = $dbh->prepare($SQL);
 $sth->execute($name);
-$result = $sth->fetchall_arrayref->[0][0];
+my $result = $sth->fetchall_arrayref->[0][0];
 $t = 'Fetching ASCII string from the database returns proper string';
 is ($result, $name, $t);
 $t = 'Fetching ASCII string from the database returns string with UTF-8 flag on';
-- 
1.8.4