Merge pull request #4979 from element-hq/renovate/major-okhttp-monorepo
Update dependency com.squareup.okhttp3:okhttp-bom to v5
This commit is contained in:
@@ -1,290 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.impl.reporter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.internal.Util;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSink;
|
||||
import okio.ByteString;
|
||||
|
||||
// simplified version of MultipartBody (OkHttp 3.6.0)
|
||||
public class BugReporterMultipartBody extends RequestBody {
|
||||
|
||||
/**
|
||||
* Listener
|
||||
*/
|
||||
public interface WriteListener {
|
||||
/**
|
||||
* Upload listener
|
||||
*
|
||||
* @param totalWritten total written bytes
|
||||
* @param contentLength content length
|
||||
*/
|
||||
void onWrite(long totalWritten, long contentLength);
|
||||
}
|
||||
|
||||
private static final MediaType FORM = MediaType.parse("multipart/form-data");
|
||||
|
||||
private static final byte[] COLONSPACE = {':', ' '};
|
||||
private static final byte[] CRLF = {'\r', '\n'};
|
||||
private static final byte[] DASHDASH = {'-', '-'};
|
||||
|
||||
private final ByteString mBoundary;
|
||||
private final MediaType mContentType;
|
||||
private final List<Part> mParts;
|
||||
private long mContentLength = -1L;
|
||||
|
||||
// listener
|
||||
private WriteListener mWriteListener;
|
||||
|
||||
//
|
||||
private List<Long> mContentLengthSize = null;
|
||||
|
||||
private BugReporterMultipartBody(ByteString boundary, List<Part> parts) {
|
||||
mBoundary = boundary;
|
||||
mContentType = MediaType.parse(FORM + "; boundary=" + boundary.utf8());
|
||||
mParts = Util.toImmutableList(parts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return mContentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
long result = mContentLength;
|
||||
if (result != -1L) return result;
|
||||
return mContentLength = writeOrCountBytes(null, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(BufferedSink sink) throws IOException {
|
||||
writeOrCountBytes(sink, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the listener
|
||||
*
|
||||
* @param listener the
|
||||
*/
|
||||
public void setWriteListener(WriteListener listener) {
|
||||
mWriteListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn the listener that some bytes have been written
|
||||
*
|
||||
* @param totalWrittenBytes the total written bytes
|
||||
*/
|
||||
private void onWrite(long totalWrittenBytes) {
|
||||
if ((null != mWriteListener) && (mContentLength > 0)) {
|
||||
mWriteListener.onWrite(totalWrittenBytes, mContentLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Either writes this request to {@code sink} or measures its content length. We have one method
|
||||
* do double-duty to make sure the counting and content are consistent, particularly when it comes
|
||||
* to awkward operations like measuring the encoded length of header strings, or the
|
||||
* length-in-digits of an encoded integer.
|
||||
*/
|
||||
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
|
||||
long byteCount = 0L;
|
||||
|
||||
Buffer byteCountBuffer = null;
|
||||
if (countBytes) {
|
||||
sink = byteCountBuffer = new Buffer();
|
||||
mContentLengthSize = new ArrayList<>();
|
||||
}
|
||||
|
||||
for (int p = 0, partCount = mParts.size(); p < partCount; p++) {
|
||||
Part part = mParts.get(p);
|
||||
Headers headers = part.headers;
|
||||
RequestBody body = part.body;
|
||||
|
||||
sink.write(DASHDASH);
|
||||
sink.write(mBoundary);
|
||||
sink.write(CRLF);
|
||||
|
||||
if (headers != null) {
|
||||
for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
|
||||
sink.writeUtf8(headers.name(h))
|
||||
.write(COLONSPACE)
|
||||
.writeUtf8(headers.value(h))
|
||||
.write(CRLF);
|
||||
}
|
||||
}
|
||||
|
||||
MediaType contentType = body.contentType();
|
||||
if (contentType != null) {
|
||||
sink.writeUtf8("Content-Type: ")
|
||||
.writeUtf8(contentType.toString())
|
||||
.write(CRLF);
|
||||
}
|
||||
|
||||
int contentLength = (int) body.contentLength();
|
||||
if (contentLength != -1) {
|
||||
sink.writeUtf8("Content-Length: ")
|
||||
.writeUtf8(contentLength + "")
|
||||
.write(CRLF);
|
||||
} else if (countBytes) {
|
||||
// We can't measure the body's size without the sizes of its components.
|
||||
byteCountBuffer.clear();
|
||||
return -1L;
|
||||
}
|
||||
|
||||
sink.write(CRLF);
|
||||
|
||||
if (countBytes) {
|
||||
byteCount += contentLength;
|
||||
mContentLengthSize.add(byteCount);
|
||||
} else {
|
||||
body.writeTo(sink);
|
||||
|
||||
// warn the listener of upload progress
|
||||
// sink.buffer().size() does not give the right value
|
||||
// assume that some data are popped
|
||||
if ((null != mContentLengthSize) && (p < mContentLengthSize.size())) {
|
||||
onWrite(mContentLengthSize.get(p));
|
||||
}
|
||||
}
|
||||
sink.write(CRLF);
|
||||
}
|
||||
|
||||
sink.write(DASHDASH);
|
||||
sink.write(mBoundary);
|
||||
sink.write(DASHDASH);
|
||||
sink.write(CRLF);
|
||||
|
||||
if (countBytes) {
|
||||
byteCount += byteCountBuffer.size();
|
||||
byteCountBuffer.clear();
|
||||
}
|
||||
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
private static void appendQuotedString(StringBuilder target, String key) {
|
||||
target.append('"');
|
||||
for (int i = 0, len = key.length(); i < len; i++) {
|
||||
char ch = key.charAt(i);
|
||||
switch (ch) {
|
||||
case '\n':
|
||||
target.append("%0A");
|
||||
break;
|
||||
case '\r':
|
||||
target.append("%0D");
|
||||
break;
|
||||
case '"':
|
||||
target.append("%22");
|
||||
break;
|
||||
default:
|
||||
target.append(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
target.append('"');
|
||||
}
|
||||
|
||||
public static final class Part {
|
||||
public static Part create(Headers headers, RequestBody body) {
|
||||
if (body == null) {
|
||||
throw new NullPointerException("body == null");
|
||||
}
|
||||
if (headers != null && headers.get("Content-Type") != null) {
|
||||
throw new IllegalArgumentException("Unexpected header: Content-Type");
|
||||
}
|
||||
if (headers != null && headers.get("Content-Length") != null) {
|
||||
throw new IllegalArgumentException("Unexpected header: Content-Length");
|
||||
}
|
||||
return new Part(headers, body);
|
||||
}
|
||||
|
||||
public static Part createFormData(String name, String value) {
|
||||
return createFormData(name, null, RequestBody.create(value, null));
|
||||
}
|
||||
|
||||
public static Part createFormData(String name, String filename, RequestBody body) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name == null");
|
||||
}
|
||||
StringBuilder disposition = new StringBuilder("form-data; name=");
|
||||
appendQuotedString(disposition, name);
|
||||
|
||||
if (filename != null) {
|
||||
disposition.append("; filename=");
|
||||
appendQuotedString(disposition, filename);
|
||||
}
|
||||
|
||||
return create(Headers.of("Content-Disposition", disposition.toString()), body);
|
||||
}
|
||||
|
||||
final Headers headers;
|
||||
final RequestBody body;
|
||||
|
||||
private Part(Headers headers, RequestBody body) {
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final ByteString boundary;
|
||||
private final List<Part> parts = new ArrayList<>();
|
||||
|
||||
public Builder() {
|
||||
this(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
public Builder(String boundary) {
|
||||
this.boundary = ByteString.encodeUtf8(boundary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a form data part to the body.
|
||||
*/
|
||||
public Builder addFormDataPart(String name, String value) {
|
||||
return addPart(Part.createFormData(name, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a form data part to the body.
|
||||
*/
|
||||
public Builder addFormDataPart(String name, String filename, RequestBody body) {
|
||||
return addPart(Part.createFormData(name, filename, body));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a part to the body.
|
||||
*/
|
||||
public Builder addPart(Part part) {
|
||||
if (part == null) throw new NullPointerException("part == null");
|
||||
parts.add(part);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the specified parts into a request body.
|
||||
*/
|
||||
public BugReporterMultipartBody build() {
|
||||
if (parts.isEmpty()) {
|
||||
throw new IllegalStateException("Multipart body must have at least one part.");
|
||||
}
|
||||
return new BugReporterMultipartBody(boundary, parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Square, Inc.
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
@file:Suppress(
|
||||
"unused",
|
||||
"KDocUnresolvedReference",
|
||||
"SpellCheckingInspection",
|
||||
)
|
||||
|
||||
package io.element.android.features.rageshake.impl.reporter
|
||||
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody
|
||||
import okio.Buffer
|
||||
import okio.BufferedSink
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Copy of [okhttp3.MultipartBody] with addition of a listener to track progress (Last imported from OkHttp 5.0.0).
|
||||
* Patches are surrounded by ELEMENT-START and ELEMENT-END
|
||||
*
|
||||
* An [RFC 2387][rfc_2387]-compliant request body.
|
||||
*
|
||||
* [rfc_2387]: http://www.ietf.org/rfc/rfc2387.txt
|
||||
*/
|
||||
@Suppress("NAME_SHADOWING")
|
||||
class BugReporterMultipartBody internal constructor(
|
||||
private val boundaryByteString: ByteString,
|
||||
@get:JvmName("type") val type: MediaType,
|
||||
@get:JvmName("parts") val parts: List<Part>,
|
||||
) : RequestBody() {
|
||||
// ELEMENT-START
|
||||
private var listener: BugReporterMultipartBodyListener? = null
|
||||
|
||||
private fun onWrite(totalWrittenBytes: Long) {
|
||||
listener
|
||||
?.takeIf { contentLength > 0 }
|
||||
?.onWrite(totalWrittenBytes, contentLength)
|
||||
}
|
||||
|
||||
private val contentLengthSize = mutableListOf<Long>()
|
||||
|
||||
fun setWriteListener(listener: BugReporterMultipartBodyListener?) {
|
||||
this.listener = listener
|
||||
}
|
||||
// ELEMENT-END
|
||||
|
||||
private val contentType: MediaType = "$type; boundary=$boundary".toMediaType()
|
||||
private var contentLength = -1L
|
||||
|
||||
@get:JvmName("boundary")
|
||||
val boundary: String
|
||||
get() = boundaryByteString.utf8()
|
||||
|
||||
/** The number of parts in this multipart body. */
|
||||
@get:JvmName("size")
|
||||
val size: Int
|
||||
get() = parts.size
|
||||
|
||||
fun part(index: Int): Part = parts[index]
|
||||
|
||||
override fun isOneShot(): Boolean = parts.any { it.body.isOneShot() }
|
||||
|
||||
/** A combination of [type] and [boundaryByteString]. */
|
||||
override fun contentType(): MediaType = contentType
|
||||
|
||||
@JvmName("-deprecated_type")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "type"),
|
||||
level = DeprecationLevel.ERROR,
|
||||
)
|
||||
fun type(): MediaType = type
|
||||
|
||||
@JvmName("-deprecated_boundary")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "boundary"),
|
||||
level = DeprecationLevel.ERROR,
|
||||
)
|
||||
fun boundary(): String = boundary
|
||||
|
||||
@JvmName("-deprecated_size")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "size"),
|
||||
level = DeprecationLevel.ERROR,
|
||||
)
|
||||
fun size(): Int = size
|
||||
|
||||
@JvmName("-deprecated_parts")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "parts"),
|
||||
level = DeprecationLevel.ERROR,
|
||||
)
|
||||
fun parts(): List<Part> = parts
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun contentLength(): Long {
|
||||
var result = contentLength
|
||||
if (result == -1L) {
|
||||
result = writeOrCountBytes(null, true)
|
||||
contentLength = result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
writeOrCountBytes(sink, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Either writes this request to [sink] or measures its content length. We have one method do
|
||||
* double-duty to make sure the counting and content are consistent, particularly when it comes
|
||||
* to awkward operations like measuring the encoded length of header strings, or the
|
||||
* length-in-digits of an encoded integer.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
private fun writeOrCountBytes(
|
||||
sink: BufferedSink?,
|
||||
countBytes: Boolean,
|
||||
): Long {
|
||||
var sink = sink
|
||||
var byteCount = 0L
|
||||
|
||||
var byteCountBuffer: Buffer? = null
|
||||
if (countBytes) {
|
||||
byteCountBuffer = Buffer()
|
||||
sink = byteCountBuffer
|
||||
// ELEMENT-START
|
||||
contentLengthSize.clear()
|
||||
// ELEMENT-END
|
||||
}
|
||||
|
||||
for (p in 0 until parts.size) {
|
||||
val part = parts[p]
|
||||
val headers = part.headers
|
||||
val body = part.body
|
||||
|
||||
sink!!.write(DASHDASH)
|
||||
sink.write(boundaryByteString)
|
||||
sink.write(CRLF)
|
||||
|
||||
if (headers != null) {
|
||||
for (h in 0 until headers.size) {
|
||||
sink
|
||||
.writeUtf8(headers.name(h))
|
||||
.write(COLONSPACE)
|
||||
.writeUtf8(headers.value(h))
|
||||
.write(CRLF)
|
||||
}
|
||||
}
|
||||
|
||||
val contentType = body.contentType()
|
||||
if (contentType != null) {
|
||||
sink
|
||||
.writeUtf8("Content-Type: ")
|
||||
.writeUtf8(contentType.toString())
|
||||
.write(CRLF)
|
||||
}
|
||||
|
||||
// We can't measure the body's size without the sizes of its components.
|
||||
val contentLength = body.contentLength()
|
||||
if (contentLength == -1L && countBytes) {
|
||||
byteCountBuffer!!.clear()
|
||||
return -1L
|
||||
}
|
||||
|
||||
sink.write(CRLF)
|
||||
|
||||
if (countBytes) {
|
||||
byteCount += contentLength
|
||||
// ELEMENT-START
|
||||
contentLengthSize.add(byteCount)
|
||||
// ELEMENT-END
|
||||
} else {
|
||||
body.writeTo(sink)
|
||||
// ELEMENT-START
|
||||
// warn the listener of upload progress
|
||||
// sink.buffer().size() does not give the right value
|
||||
// assume that some data are popped
|
||||
contentLengthSize.getOrNull(p)?.let { writtenByte ->
|
||||
onWrite(writtenByte)
|
||||
}
|
||||
// ELEMENT-END
|
||||
}
|
||||
|
||||
sink.write(CRLF)
|
||||
}
|
||||
|
||||
sink!!.write(DASHDASH)
|
||||
sink.write(boundaryByteString)
|
||||
sink.write(DASHDASH)
|
||||
sink.write(CRLF)
|
||||
|
||||
if (countBytes) {
|
||||
byteCount += byteCountBuffer!!.size
|
||||
byteCountBuffer.clear()
|
||||
}
|
||||
|
||||
return byteCount
|
||||
}
|
||||
|
||||
class Part private constructor(
|
||||
@get:JvmName("headers") val headers: Headers?,
|
||||
@get:JvmName("body") val body: RequestBody,
|
||||
) {
|
||||
@JvmName("-deprecated_headers")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "headers"),
|
||||
level = DeprecationLevel.ERROR,
|
||||
)
|
||||
fun headers(): Headers? = headers
|
||||
|
||||
@JvmName("-deprecated_body")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "body"),
|
||||
level = DeprecationLevel.ERROR,
|
||||
)
|
||||
fun body(): RequestBody = body
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(body: RequestBody): Part = create(null, body)
|
||||
|
||||
@JvmStatic
|
||||
fun create(
|
||||
headers: Headers?,
|
||||
body: RequestBody,
|
||||
): Part {
|
||||
require(headers?.get("Content-Type") == null) { "Unexpected header: Content-Type" }
|
||||
require(headers?.get("Content-Length") == null) { "Unexpected header: Content-Length" }
|
||||
return Part(headers, body)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createFormData(
|
||||
name: String,
|
||||
value: String,
|
||||
): Part = createFormData(name, null, value.toRequestBody())
|
||||
|
||||
@JvmStatic
|
||||
fun createFormData(
|
||||
name: String,
|
||||
filename: String?,
|
||||
body: RequestBody,
|
||||
): Part {
|
||||
val disposition =
|
||||
buildString {
|
||||
append("form-data; name=")
|
||||
appendQuotedString(name)
|
||||
|
||||
if (filename != null) {
|
||||
append("; filename=")
|
||||
appendQuotedString(filename)
|
||||
}
|
||||
}
|
||||
|
||||
val headers =
|
||||
Headers
|
||||
.Builder()
|
||||
.addUnsafeNonAscii("Content-Disposition", disposition)
|
||||
.build()
|
||||
|
||||
return create(headers, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Builder
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
boundary: String = UUID.randomUUID().toString(),
|
||||
) {
|
||||
private val boundary: ByteString = boundary.encodeUtf8()
|
||||
|
||||
// ELEMENT-START
|
||||
// Element: use FORM as default type
|
||||
private var type = FORM
|
||||
// ELEMENT-END
|
||||
|
||||
private val parts = mutableListOf<Part>()
|
||||
|
||||
/**
|
||||
* Set the MIME type. Expected values for `type` are [MIXED] (the default), [ALTERNATIVE],
|
||||
* [DIGEST], [PARALLEL] and [FORM].
|
||||
*/
|
||||
fun setType(type: MediaType) =
|
||||
apply {
|
||||
require(type.type == "multipart") { "multipart != $type" }
|
||||
this.type = type
|
||||
}
|
||||
|
||||
/** Add a part to the body. */
|
||||
fun addPart(body: RequestBody) =
|
||||
apply {
|
||||
addPart(Part.create(body))
|
||||
}
|
||||
|
||||
/** Add a part to the body. */
|
||||
fun addPart(
|
||||
headers: Headers?,
|
||||
body: RequestBody,
|
||||
) = apply {
|
||||
addPart(Part.create(headers, body))
|
||||
}
|
||||
|
||||
/** Add a form data part to the body. */
|
||||
fun addFormDataPart(
|
||||
name: String,
|
||||
value: String,
|
||||
) = apply {
|
||||
addPart(Part.createFormData(name, value))
|
||||
}
|
||||
|
||||
/** Add a form data part to the body. */
|
||||
fun addFormDataPart(
|
||||
name: String,
|
||||
filename: String?,
|
||||
body: RequestBody,
|
||||
) = apply {
|
||||
addPart(Part.createFormData(name, filename, body))
|
||||
}
|
||||
|
||||
/** Add a part to the body. */
|
||||
fun addPart(part: Part) =
|
||||
apply {
|
||||
parts += part
|
||||
}
|
||||
|
||||
/** Assemble the specified parts into a request body. */
|
||||
fun build(): BugReporterMultipartBody {
|
||||
check(parts.isNotEmpty()) { "Multipart body must have at least one part." }
|
||||
return BugReporterMultipartBody(boundary, type, parts.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The "mixed" subtype of "multipart" is intended for use when the body parts are independent
|
||||
* and need to be bundled in a particular order. Any "multipart" subtypes that an implementation
|
||||
* does not recognize must be treated as being of subtype "mixed".
|
||||
*/
|
||||
@JvmField
|
||||
val MIXED = "multipart/mixed".toMediaType()
|
||||
|
||||
/**
|
||||
* The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the
|
||||
* semantics are different. In particular, each of the body parts is an "alternative" version of
|
||||
* the same information.
|
||||
*/
|
||||
@JvmField
|
||||
val ALTERNATIVE = "multipart/alternative".toMediaType()
|
||||
|
||||
/**
|
||||
* This type is syntactically identical to "multipart/mixed", but the semantics are different.
|
||||
* In particular, in a digest, the default `Content-Type` value for a body part is changed from
|
||||
* "text/plain" to "message/rfc822".
|
||||
*/
|
||||
@JvmField
|
||||
val DIGEST = "multipart/digest".toMediaType()
|
||||
|
||||
/**
|
||||
* This type is syntactically identical to "multipart/mixed", but the semantics are different.
|
||||
* In particular, in a parallel entity, the order of body parts is not significant.
|
||||
*/
|
||||
@JvmField
|
||||
val PARALLEL = "multipart/parallel".toMediaType()
|
||||
|
||||
/**
|
||||
* The media-type multipart/form-data follows the rules of all multipart MIME data streams as
|
||||
* outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who
|
||||
* fills out the form. Each field has a name. Within a given form, the names are unique.
|
||||
*/
|
||||
@JvmField
|
||||
val FORM = "multipart/form-data".toMediaType()
|
||||
|
||||
private val COLONSPACE = byteArrayOf(':'.code.toByte(), ' '.code.toByte())
|
||||
private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
|
||||
private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte())
|
||||
|
||||
/**
|
||||
* Appends a quoted-string to a StringBuilder.
|
||||
*
|
||||
* RFC 2388 is rather vague about how one should escape special characters in form-data
|
||||
* parameters, and as it turns out Firefox and Chrome actually do rather different things, and
|
||||
* both say in their comments that they're not really sure what the right approach is. We go
|
||||
* with Chrome's behavior (which also experimentally seems to match what IE does), but if you
|
||||
* actually want to have a good chance of things working, please avoid double-quotes, newlines,
|
||||
* percent signs, and the like in your field names.
|
||||
*/
|
||||
internal fun StringBuilder.appendQuotedString(key: String) {
|
||||
append('"')
|
||||
for (i in 0 until key.length) {
|
||||
when (val ch = key[i]) {
|
||||
'\n' -> append("%0A")
|
||||
'\r' -> append("%0D")
|
||||
'"' -> append("%22")
|
||||
else -> append(ch)
|
||||
}
|
||||
}
|
||||
append('"')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.impl.reporter
|
||||
|
||||
fun interface BugReporterMultipartBodyListener {
|
||||
/**
|
||||
* Upload listener.
|
||||
*
|
||||
* @param totalWritten total written bytes
|
||||
* @param contentLength content length
|
||||
*/
|
||||
fun onWrite(totalWritten: Long, contentLength: Long)
|
||||
}
|
||||
@@ -131,7 +131,7 @@ accompanist_permission = { module = "com.google.accompanist:accompanist-permissi
|
||||
squareup_seismic = "com.squareup:seismic:1.0.3"
|
||||
|
||||
# network
|
||||
network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.12.0"
|
||||
network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:5.0.0"
|
||||
network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" }
|
||||
network_okhttp_okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
network_okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.network.interceptors
|
||||
|
||||
import io.element.android.libraries.core.extensions.ellipsize
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
@@ -28,7 +29,7 @@ internal class FormattedJsonHttpLogger(
|
||||
*/
|
||||
@Synchronized
|
||||
override fun log(message: String) {
|
||||
Timber.v(message)
|
||||
Timber.v(message.ellipsize(200_000))
|
||||
|
||||
// Try to log formatted Json only if there is a chance that [message] contains Json.
|
||||
// It can be only the case if we log the bodies of Http requests.
|
||||
|
||||
Reference in New Issue
Block a user