/*
* linux/net/sunrpc/xdr.c
*
* Generic XDR support.
*
* Copyright (C) 1995, 1996 Olaf Kirch <okir@monad.swb.de>
*/
#include <linux/types.h>
#include <linux/socket.h>
#include <linux/string.h>
#include <linux/kernel.h>
#include <linux/pagemap.h>
#include <linux/errno.h>
#include <linux/in.h>
#include <linux/net.h>
#include <net/sock.h>
#include <linux/sunrpc/xdr.h>
#include <linux/sunrpc/msg_prot.h>
/*
* XDR functions for basic NFS types
*/
u32 *
xdr_encode_netobj(u32 *p, const struct xdr_netobj *obj)
{
unsigned int quadlen = XDR_QUADLEN(obj->len);
p[quadlen] = 0; /* zero trailing bytes */
*p++ = htonl(obj->len);
memcpy(p, obj->data, obj->len);
return p + XDR_QUADLEN(obj->len);
}
u32 *
xdr_decode_netobj_fixed(u32 *p, void *obj, unsigned int len)
{
if (ntohl(*p++) != len)
return NULL;
memcpy(obj, p, len);
return p + XDR_QUADLEN(len);
}
u32 *
xdr_decode_netobj(u32 *p, struct xdr_netobj *obj)
{
unsigned int len;
if ((len = ntohl(*p++)) > XDR_MAX_NETOBJ)
return NULL;
obj->len = len;
obj->data = (u8 *) p;
return p + XDR_QUADLEN(len);
}
u32 *
xdr_encode_array(u32 *p, const char *array, unsigned int len)
{
int quadlen = XDR_QUADLEN(len);
p[quadlen] = 0;
*p++ = htonl(len);
memcpy(p, array, len);
return p + quadlen;
}
u32 *
xdr_encode_string(u32 *p, const char *string)
{
return xdr_encode_array(p, string, strlen(string));
}
u32 *
xdr_decode_string(u32 *p, char **sp, int *lenp, int maxlen)
{
unsigned int len;
char *string;
if ((len = ntohl(*p++)) > maxlen)
return NULL;
if (lenp)
*lenp = len;
if ((len % 4) != 0) {
string = (char *) p;
} else {
string = (char *) (p - 1);
memmove(string, p, len);
}
string[len] = '\0';
*sp = string;
return p + XDR_QUADLEN(len);
}
u32 *
xdr_decode_string_inplace(u32 *p, char **sp, int *lenp, int maxlen)
{
unsigned int len;
if ((len = ntohl(*p++)) > maxlen)
return NULL;
*lenp = len;
*sp = (char *) p;
return p + XDR_QUADLEN(len);
}
void
xdr_encode_pages(struct xdr_buf *xdr, struct page **pages, unsigned int base,
unsigned int len)
{
xdr->pages = pages;
xdr->page_base = base;
xdr->page_len = len;
if (len & 3) {
struct iovec *iov = xdr->tail;
unsigned int pad = 4 - (len & 3);
iov->iov_base = (void *) "\0\0\0";
iov->iov_len = pad;
len += pad;
}
xdr->len += len;
}
void
xdr_inline_pages(struct xdr_buf *xdr, unsigned int offset,
struct page **pages, unsigned int base, unsigned int len)
{
struct iovec *head = xdr->head;
struct iovec *tail = xdr->tail;
char *buf = (char *)head->iov_base;
unsigned int buflen = head->iov_len;
head->iov_len = offset;
xdr->pages = pages;
xdr->page_base = base;
xdr->page_len = len;
tail->iov_base = buf + offset;
tail->iov_len = buflen - offset;
xdr->len += len;
}
/*
* Realign the iovec if the server missed out some reply elements
* (such as post-op attributes,...)
* Note: This is a simple implementation that assumes that
* len <= iov->iov_len !!!
* The RPC header (assumed to be the 1st element in the iov array)
* is not shifted.
*/
void xdr_shift_iovec(struct iovec *iov, int nr, size_t len)
{
struct iovec *pvec;
for (pvec = iov + nr - 1; nr > 1; nr--, pvec--) {
struct iovec *svec = pvec - 1;
if (len > pvec->iov_len) {
printk(KERN_DEBUG "RPC: Urk! Large shift of short iovec.\n");
return;
}
memmove((char *)pvec->iov_base + len, pvec->iov_base,
pvec->iov_len - len);
if (len > svec->iov_len) {
printk(KERN_DEBUG "RPC: Urk! Large shift of short iovec.\n");
return;
}
memcpy(pvec->iov_base,
(char *)svec->iov_base + svec->iov_len - len, len);
}
}
/*
* Map a struct xdr_buf into an iovec array.
*/
int xdr_kmap(struct iovec *iov_base, struct xdr_buf *xdr, size_t base)
{
struct iovec *iov = iov_base;
struct page **ppage = xdr->pages;
unsigned int len, pglen = xdr->page_len;
len = xdr->head[0].iov_len;
if (base < len) {
iov->iov_len = len - base;
iov->iov_base = (char *)xdr->head[0].iov_base + base;
iov++;
base = 0;
} else
base -= len;
if (pglen == 0)
goto map_tail;
if (base >= pglen) {
base -= pglen;
goto map_tail;
}
if (base || xdr->page_base) {
pglen -= base;
base += xdr->page_base;
ppage += base >> PAGE_CACHE_SHIFT;
base &= ~PAGE_CACHE_MASK;
}
do {
len = PAGE_CACHE_SIZE;
iov->iov_base = kmap(*ppage);
if (base) {
iov->iov_base += base;
len -= base;
base = 0;
}
if (pglen < len)
len = pglen;
iov->iov_len = len;
iov++;
ppage++;
} while ((pglen -= len) != 0);
map_tail:
if (xdr->tail[0].iov_len) {
iov->iov_len = xdr->tail[0].iov_len - base;
iov->iov_base = (char *)xdr->tail[0].iov_base + base;
iov++;
}
return (iov - iov_base);
}
void xdr_kunmap(struct xdr_buf *xdr, size_t base)
{
struct page **ppage = xdr->pages;
unsigned int pglen = xdr->page_len;
if (!pglen)
return;
if (base > xdr->head[0].iov_len)
base -= xdr->head[0].iov_len;
else
base = 0;
if (base >= pglen)
return;
if (base || xdr->page_base) {
pglen -= base;
base += xdr->page_base;
ppage += base >> PAGE_CACHE_SHIFT;
/* Note: The offset means that the length of the first
* page is really (PAGE_CACHE_SIZE - (base & ~PAGE_CACHE_MASK)).
* In order to avoid an extra test inside the loop,
* we bump pglen here, and just subtract PAGE_CACHE_SIZE... */
pglen += base & ~PAGE_CACHE_MASK;
}
for (;;) {
flush_dcache_page(*ppage);
kunmap(*ppage);
if (pglen <= PAGE_CACHE_SIZE)
break;
pglen -= PAGE_CACHE_SIZE;
ppage++;
}
}
void
xdr_partial_copy_from_skb(struct xdr_buf *xdr, unsigned int base,
skb_reader_t *desc,
skb_read_actor_t copy_actor)
{
struct page **ppage = xdr->pages;
unsigned int len, pglen = xdr->page_len;
int ret;
len = xdr->head[0].iov_len;
if (base < len) {
len -= base;
ret = copy_actor(desc, (char *)xdr->head[0].iov_base + base, len);
if (ret != len || !desc->count)
return;
base = 0;
} else
base -= len;
if (pglen == 0)
goto copy_tail;
if (base >= pglen) {
base -= pglen;
goto copy_tail;
}
if (base || xdr->page_base) {
pglen -= base;
base += xdr->page_base;
ppage += base >> PAGE_CACHE_SHIFT;
base &= ~PAGE_CACHE_MASK;
}
do {
char *kaddr;
len = PAGE_CACHE_SIZE;
kaddr = kmap_atomic(*ppage, KM_SKB_SUNRPC_DATA);
if (base) {
len -= base;
if (pglen < len)
len = pglen;
ret = copy_actor(desc, kaddr + base, len);
base = 0;
} else {
if (pglen < len)
len = pglen;
ret = copy_actor(desc, kaddr, len);
}
flush_dcache_page(*ppage);
kunmap_atomic(kaddr, KM_SKB_SUNRPC_DATA);
if (ret != len || !desc->count)
return;
ppage++;
} while ((pglen -= len) != 0);
copy_tail:
len = xdr->tail[0].iov_len;
if (base < len)
copy_actor(desc, (char *)xdr->tail[0].iov_base + base, len - base);
}
int
xdr_sendpages(struct socket *sock, struct sockaddr *addr, int addrlen,
struct xdr_buf *xdr, unsigned int base, int msgflags)
{
struct page **ppage = xdr->pages;
unsigned int len, pglen = xdr->page_len;
int err, ret = 0;
ssize_t (*sendpage)(struct socket *, struct page *, int, size_t, int);
mm_segment_t oldfs;
len = xdr->head[0].iov_len;
if (base < len || (addr != NULL && base == 0)) {
struct iovec iov = {
.iov_base = xdr->head[0].iov_base + base,
.iov_len = len - base,
};
struct msghdr msg = {
.msg_name = addr,
.msg_namelen = addrlen,
.msg_flags = msgflags,
};
if (iov.iov_len != 0) {
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
}
if (xdr->len > len)
msg.msg_flags |= MSG_MORE;
oldfs = get_fs(); set_fs(get_ds());
err = sock_sendmsg(sock, &msg, iov.iov_len);
set_fs(oldfs);
if (ret == 0)
ret = err;
else if (err > 0)
ret += err;
if (err != iov.iov_len)
goto out;
base = 0;
} else
base -= len;
if (pglen == 0)
goto copy_tail;
if (base >= pglen) {
base -= pglen;
goto copy_tail;
}
if (base || xdr->page_base) {
pglen -= base;
base += xdr->page_base;
ppage += base >> PAGE_CACHE_SHIFT;
base &= ~PAGE_CACHE_MASK;
}
sendpage = sock->ops->sendpage ? : sock_no_sendpage;
do {
int flags = msgflags;
len = PAGE_CACHE_SIZE;
if (base)
len -= base;
if (pglen < len)
len = pglen;
if (pglen != len || xdr->tail[0].iov_len != 0)
flags |= MSG_MORE;
/* Hmm... We might be dealing with highmem pages */
if (PageHighMem(*ppage))
sendpage = sock_no_sendpage;
err = sendpage(sock, *ppage, base, len, flags);
if (ret == 0)
ret = err;
else if (err > 0)
ret += err;
if (err != len)
goto out;
base = 0;
ppage++;
} while ((pglen -= len) != 0);
copy_tail:
len = xdr->tail[0].iov_len;
if (base < len) {
struct iovec iov = {
.iov_base = xdr->tail[0].iov_base + base,
.iov_len = len - base,
};
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_flags = msgflags,
};
oldfs = get_fs(); set_fs(get_ds());
err = sock_sendmsg(sock, &msg, iov.iov_len);
set_fs(oldfs);
if (ret == 0)
ret = err;
else if (err > 0)
ret += err;
}
out:
return ret;
}
/*
* Helper routines for doing 'memmove' like operations on a struct xdr_buf
*
* _shift_data_right_pages
* @pages: vector of pages containing both the source and dest memory area.
* @pgto_base: page vector address of destination
* @pgfrom_base: page vector address of source
* @len: number of bytes to copy
*
* Note: the addresses pgto_base and pgfrom_base are both calculated in
* the same way:
* if a memory area starts at byte 'base' in page 'pages[i]',
* then its address is given as (i << PAGE_CACHE_SHIFT) + base
* Also note: pgfrom_base must be < pgto_base, but the memory areas
* they point to may overlap.
*/
static void
_shift_data_right_pages(struct page **pages, size_t pgto_base,
size_t pgfrom_base, size_t len)
{
struct page **pgfrom, **pgto;
char *vfrom, *vto;
size_t copy;
BUG_ON(pgto_base <= pgfrom_base);
pgto_base += len;
pgfrom_base += len;
pgto = pages + (pgto_base >> PAGE_CACHE_SHIFT);
pgfrom = pages + (pgfrom_base >> PAGE_CACHE_SHIFT);
pgto_base &= ~PAGE_CACHE_MASK;
pgfrom_base &= ~PAGE_CACHE_MASK;
do {
/* Are any pointers crossing a page boundary? */
if (pgto_base == 0) {
pgto_base = PAGE_CACHE_SIZE;
pgto--;
}
if (pgfrom_base == 0) {
pgfrom_base = PAGE_CACHE_SIZE;
pgfrom--;
}
copy = len;
if (copy > pgto_base)
copy = pgto_base;
if (copy > pgfrom_base)
copy = pgfrom_base;
pgto_base -= copy;
pgfrom_base -= copy;
vto = kmap_atomic(*pgto, KM_USER0);
vfrom = kmap_atomic(*pgfrom, KM_USER1);
memmove(vto + pgto_base, vfrom + pgfrom_base, copy);
kunmap_atomic(vfrom, KM_USER1);
kunmap_atomic(vto, KM_USER0);
} while ((len -= copy) != 0);
}
/*
* _copy_to_pages
* @pages: array of pages
* @pgbase: page vector address of destination
* @p: pointer to source data
* @len: length
*
* Copies data from an arbitrary memory location into an array of pages
* The copy is assumed to be non-overlapping.
*/
static void
_copy_to_pages(struct page **pages, size_t pgbase, const char *p, size_t len)
{
struct page **pgto;
char *vto;
size_t copy;
pgto = pages + (pgbase >> PAGE_CACHE_SHIFT);
pgbase &= ~PAGE_CACHE_MASK;
do {
copy = PAGE_CACHE_SIZE - pgbase;
if (copy > len)
copy = len;
vto = kmap_atomic(*pgto, KM_USER0);
memcpy(vto + pgbase, p, copy);
kunmap_atomic(vto, KM_USER0);
pgbase += copy;
if (pgbase == PAGE_CACHE_SIZE) {
pgbase = 0;
pgto++;
}
p += copy;
} while ((len -= copy) != 0);
}
/*
* _copy_from_pages
* @p: pointer to destination
* @pages: array of pages
* @pgbase: offset of source data
* @len: length
*
* Copies data into an arbitrary memory location from an array of pages
* The copy is assumed to be non-overlapping.
*/
static void
_copy_from_pages(char *p, struct page **pages, size_t pgbase, size_t len)
{
struct page **pgfrom;
char *vfrom;
size_t copy;
pgfrom = pages + (pgbase >> PAGE_CACHE_SHIFT);
pgbase &= ~PAGE_CACHE_MASK;
do {
copy = PAGE_CACHE_SIZE - pgbase;
if (copy > len)
copy = len;
vfrom = kmap_atomic(*pgfrom, KM_USER0);
memcpy(p, vfrom + pgbase, copy);
kunmap_atomic(vfrom, KM_USER0);
pgbase += copy;
if (pgbase == PAGE_CACHE_SIZE) {
pgbase = 0;
pgfrom++;
}
p += copy;
} while ((len -= copy) != 0);
}
/*
* xdr_shrink_bufhead
* @buf: xdr_buf
* @len: bytes to remove from buf->head[0]
*
* Shrinks XDR buffer's header iovec buf->head[0] by
* 'len' bytes. The extra data is not lost, but is instead
* moved into the inlined pages and/or the tail.
*/
void
xdr_shrink_bufhead(struct xdr_buf *buf, size_t len)
{
struct iovec *head, *tail;
size_t copy, offs;
unsigned int pglen = buf->page_len;
tail = buf->tail;
head = buf->head;
BUG_ON (len > head->iov_len);
/* Shift the tail first */
if (tail->iov_len != 0) {
if (tail->iov_len > len) {
copy = tail->iov_len - len;
memmove((char *)tail->iov_base + len,
tail->iov_base, copy);
}
/* Copy from the inlined pages into the tail */
copy = len;
if (copy > pglen)
copy = pglen;
offs = len - copy;
if (offs >= tail->iov_len)
copy = 0;
else if (copy > tail->iov_len - offs)
copy = tail->iov_len - offs;
if (copy != 0)
_copy_from_pages((char *)tail->iov_base + offs,
buf->pages,
buf->page_base + pglen + offs - len,
copy);
/* Do we also need to copy data from the head into the tail ? */
if (len > pglen) {
offs = copy = len - pglen;
if (copy > tail->iov_len)
copy = tail->iov_len;
memcpy(tail->iov_base,
(char *)head->iov_base +
head->iov_len - offs,
copy);
}
}
/* Now handle pages */
if (pglen != 0) {
if (pglen > len)
_shift_data_right_pages(buf->pages,
buf->page_base + len,
buf->page_base,
pglen - len);
copy = len;
if (len > pglen)
copy = pglen;
_copy_to_pages(buf->pages, buf->page_base,
(char *)head->iov_base + head->iov_len - len,
copy);
}
head->iov_len -= len;
buf->len -= len;
}
/*
* xdr_shrink_pagelen
* @buf: xdr_buf
* @len: bytes to remove from buf->pages
*
* Shrinks XDR buffer's page array buf->pages by
* 'len' bytes. The extra data is not lost, but is instead
* moved into the tail.
*/
void
xdr_shrink_pagelen(struct xdr_buf *buf, size_t len)
{
struct iovec *tail;
size_t copy;
char *p;
unsigned int pglen = buf->page_len;
tail = buf->tail;
BUG_ON (len > pglen);
/* Shift the tail first */
if (tail->iov_len != 0) {
p = (char *)tail->iov_base + len;
if (tail->iov_len > len) {
copy = tail->iov_len - len;
memmove(p, tail->iov_base, copy);
} else
buf->len -= len;
/* Copy from the inlined pages into the tail */
copy = len;
if (copy > tail->iov_len)
copy = tail->iov_len;
_copy_from_pages((char *)tail->iov_base,
buf->pages, buf->page_base + pglen - len,
copy);
}
buf->page_len -= len;
buf->len -= len;
}
void
xdr_shift_buf(struct xdr_buf *buf, size_t len)
{
xdr_shrink_bufhead(buf, len);
}
void
xdr_write_pages(struct xdr_stream *xdr, struct page **pages, unsigned int base,
unsigned int len)
{
struct xdr_buf *buf = xdr->buf;
struct iovec *iov = buf->tail;
buf->pages = pages;
buf->page_base = base;
buf->page_len = len;
iov->iov_base = (char *)xdr->p;
iov->iov_len = 0;
xdr->iov = iov;
if (len & 3) {
unsigned int pad = 4 - (len & 3);
BUG_ON(xdr->p >= xdr->end);
iov->iov_base = (char *)xdr->p + (len & 3);
iov->iov_len += pad;
len += pad;
*xdr->p++ = 0;
}
buf->len += len;
}
void
xdr_read_pages(struct xdr_stream *xdr, unsigned int len)
{
struct xdr_buf *buf = xdr->buf;
struct iovec *iov;
ssize_t shift;
int padding;
/* Realign pages to current pointer position */
iov = buf->head;
shift = iov->iov_len + (char *)iov->iov_base - (char *)xdr->p;
if (shift > 0)
xdr_shrink_bufhead(buf, shift);
/* Truncate page data and move it into the tail */
if (buf->page_len > len)
xdr_shrink_pagelen(buf, buf->page_len - len);
padding = (XDR_QUADLEN(len) << 2) - len;
xdr->iov = iov = buf->tail;
xdr->p = (uint32_t *)((char *)iov->iov_base + padding);
xdr->end = (uint32_t *)((char *)iov->iov_base + iov->iov_len);
}