blob: 9195f9751bda37ee81600aa6993149989f6d10fd [file] [log] [blame]
// Copyright 2013 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: sligocki@google.com (Shawn Ligocki)
#include "pagespeed/kernel/base/source_map.h"
#include "base/logging.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/json.h"
#include "pagespeed/kernel/base/string.h"
namespace net_instaweb {
namespace source_map {
char EncodeBase64(int val) {
// Note: This constant also exists in
// third_party/instaweb/src/third_party/base64/base64.cc
// We have elected not to share it.
static const char base64_chars[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
if (val < 0 || val >= 64) {
LOG(DFATAL) << "Invalid value passed into EncodeBase64 " << val;
return '?';
}
return base64_chars[val];
}
GoogleString EncodeVlq(const int32 input_val) {
// Base64 has 6-bits of data per char.
// For VLQ Base64 encoding, the top (6th) bit is the continuation bit and the
// bottom 5 bits are data bits.
static const int kNumDataBits = 5;
static const int kContinuationBit = 1 << kNumDataBits; // 100000
static const int kDataMask = kContinuationBit - 1; // 011111
#ifndef NDEBUG
for (int i = 0; i < kNumDataBits; ++i) {
DCHECK(kDataMask & (1 << i)) << kDataMask;
}
#endif
GoogleString result;
// We use a 64-bit int to avoid overflow issues. Note that input_val is
// restricted to 32-bit so there is significant padding.
// So (val = -val) and (val <<= 1) below are both safe.
int64 val = input_val;
// Sign is stored in low bit.
bool is_negative = (val < 0);
if (is_negative) {
val = -val;
}
val <<= 1;
if (is_negative) {
val |= 0x1;
}
DCHECK(val >= 0) << val;
// Little endian VLQ format.
while (val > kDataMask) {
int block = val & kDataMask;
block |= kContinuationBit;
result += EncodeBase64(block);
DCHECK(val >= 0) << val; // >> acts strangely with negative values.
val >>= kNumDataBits;
}
DCHECK((val & kContinuationBit) == 0) << val;
result += EncodeBase64(val);
return result;
}
// Encode to the compact mappings format, which is a ;-separated list of
// ,-separated lists of base64 VLQ values.
bool EncodeMappings(const MappingVector& mappings,
GoogleString* result) {
int current_gen_line = 0;
bool first_segment_in_line = true;
for (int i = 0, mappings_size = mappings.size(); i < mappings_size; ++i) {
if (mappings[i].gen_line < current_gen_line) {
LOG(DFATAL) << "Mappings are not sorted.";
return false;
}
// gen_line is not encoded into the fields, instead each line in the
// generated file is ; delineated in the VLQ.
while (mappings[i].gen_line > current_gen_line) {
*result += ";";
first_segment_in_line = true;
++current_gen_line;
}
// Fields to encode in base64 VLQ.
// 1) Generated column number
if (first_segment_in_line) {
// First segment for each line must list absolute column number.
*result += EncodeVlq(mappings[i].gen_col);
} else {
// Subsequent ones will list column number as a diff from previous one
// as a space saving measure.
*result += EncodeVlq(mappings[i].gen_col - mappings[i - 1].gen_col);
}
// 2) Source file number
if (i == 0) {
// First segment of the file must list absolute numbers.
*result += EncodeVlq(mappings[i].src_file);
} else {
// Subsequent ones list diffs.
*result += EncodeVlq(mappings[i].src_file - mappings[i - 1].src_file);
}
// 3) Source line number
if (i == 0) {
*result += EncodeVlq(mappings[i].src_line);
} else {
*result += EncodeVlq(mappings[i].src_line - mappings[i - 1].src_line);
}
// 4) Source column number
if (i == 0) {
*result += EncodeVlq(mappings[i].src_col);
} else {
*result += EncodeVlq(mappings[i].src_col - mappings[i - 1].src_col);
}
// Note: We do not add (5) Names.
first_segment_in_line = false;
// Segments are comma-separated, but don't add a trailing comma nor a comma
// if a semicolon (line change) will be added.
if (i + 1 < mappings_size &&
mappings[i + 1].gen_line == current_gen_line) {
*result += ",";
}
}
return true;
}
GoogleString PercentEncode(StringPiece url) {
GoogleString result;
for (int i = 0, n = url.size(); i < n; ++i) {
switch (url[i]) {
case '<':
result += "%3C";
break;
case '>':
result += "%3E";
break;
default:
result.push_back(url[i]);
break;
}
}
return result;
}
bool Encode(StringPiece generated_url,
StringPiece source_url,
const MappingVector& mappings,
GoogleString* encoded_source_map) {
GoogleString encoded_mappings;
bool success = EncodeMappings(mappings, &encoded_mappings);
if (success) {
Json::Value json;
json["version"] = 3;
if (!generated_url.empty()) {
json["file"] = PercentEncode(generated_url).c_str();
}
// Sources array with one value.
json["sources"][0] = PercentEncode(source_url).c_str();
// Note: We do not provide names functionality.
json["names"] = Json::arrayValue; // Empty array.
json["mappings"] = encoded_mappings.c_str();
// Standard XSSI protection.
// http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-xssi
*encoded_source_map += ")]}'\n";
Json::FastWriter writer;
*encoded_source_map += writer.write(json);
}
return success;
}
} // namespace source_map
} // namespace net_instaweb